From aee88ad261335506f9ee3a1598d0f939f7d0ca88 Mon Sep 17 00:00:00 2001
From: Denis Lugowski
Date: Thu, 1 May 2025 09:20:32 +0200
Subject: [PATCH] feat(fullstack): Read user out of JWT and persist with
created and last modified
---
.../api/legalconsenthub.yml | 119 +-----------------
.../application_form/ApplicationForm.kt | 20 ++-
.../application_form/ApplicationFormMapper.kt | 22 ++--
.../CustomJwtAuthenticationConverter.kt | 22 ++++
.../legalconsenthub/config/SecurityConfig.kt | 7 +-
.../security/CustomJwtAuthentication.kt | 17 +++
.../security/CustomJwtTokenPrincipal.kt | 6 +
.../legalconsenthub/user/User.kt | 9 ++
.../legalconsenthub/user/UserMapper.kt | 21 ++++
.../src/main/resources/application.yaml | 8 --
.../resources/db/migrations/001-schema.sql | 16 +--
.../pages/application-forms/[id].vue | 2 +-
legalconsenthub/pages/index.vue | 4 +-
legalconsenthub/server/utils/auth.ts | 3 +-
14 files changed, 129 insertions(+), 147 deletions(-)
create mode 100644 legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/CustomJwtAuthenticationConverter.kt
create mode 100644 legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtAuthentication.kt
create mode 100644 legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtTokenPrincipal.kt
create mode 100644 legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/User.kt
create mode 100644 legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserMapper.kt
diff --git a/legalconsenthub-backend/api/legalconsenthub.yml b/legalconsenthub-backend/api/legalconsenthub.yml
index b40493e..273875c 100644
--- a/legalconsenthub-backend/api/legalconsenthub.yml
+++ b/legalconsenthub-backend/api/legalconsenthub.yml
@@ -240,48 +240,6 @@ paths:
"503":
$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}:
parameters:
- name: id
@@ -607,77 +565,12 @@ components:
type: object
required:
- id
- - username
- - firstname
- - lastname
- - email
- - password
- - roleId
- - createdAt
- - modifiedAt
+ - name
properties:
id:
type: string
- format: uuid
- username:
+ name:
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:
@@ -704,9 +597,9 @@ components:
isTemplate:
type: boolean
createdBy:
- type: string
+ $ref: "#/components/schemas/UserDto"
lastModifiedBy:
- type: string
+ $ref: "#/components/schemas/UserDto"
createdAt:
type: string
format: date-time
@@ -732,10 +625,6 @@ components:
isTemplate:
type: boolean
default: false
- createdBy:
- type: string
- lastModifiedBy:
- type: string
PagedApplicationFormDto:
type: object
diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationForm.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationForm.kt
index 9fb9c37..b605875 100644
--- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationForm.kt
+++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationForm.kt
@@ -1,6 +1,9 @@
package com.betriebsratkanzlei.legalconsenthub.application_form
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.Column
import jakarta.persistence.Entity
@@ -8,6 +11,7 @@ import jakarta.persistence.EntityListeners
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id
import jakarta.persistence.OneToMany
+import jakarta.persistence.Embedded
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
@@ -30,11 +34,19 @@ class ApplicationForm(
@Column(nullable = false)
var isTemplate: Boolean,
- @Column(nullable = false)
- var createdBy: String = "",
+ @Embedded
+ @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)
- var lastModifiedBy: String = "",
+ @Embedded
+ @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
@Column(nullable = false)
diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormMapper.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormMapper.kt
index 21857c8..d962e52 100644
--- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormMapper.kt
+++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormMapper.kt
@@ -1,20 +1,24 @@
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.CreateApplicationFormDto
+import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import java.time.LocalDateTime
@Component
-class ApplicationFormMapper(private val formElementMapper: FormElementMapper) {
+class ApplicationFormMapper(private val formElementMapper: FormElementMapper, private val userMapper: UserMapper) {
fun toApplicationFormDto(applicationForm: ApplicationForm): ApplicationFormDto {
return ApplicationFormDto(
id = applicationForm.id ?: throw IllegalStateException("ApplicationForm ID must not be null!"),
name = applicationForm.name,
formElements = applicationForm.formElements.map { formElementMapper.toFormElementDto(it) },
isTemplate = applicationForm.isTemplate,
- createdBy = applicationForm.createdBy,
- lastModifiedBy = applicationForm.lastModifiedBy,
+ createdBy = userMapper.toUserDto(applicationForm.createdBy),
+ lastModifiedBy = userMapper.toUserDto(applicationForm.lastModifiedBy),
createdAt = applicationForm.createdAt ?: LocalDateTime.now(),
modifiedAt = applicationForm.modifiedAt ?: LocalDateTime.now()
)
@@ -26,19 +30,23 @@ class ApplicationFormMapper(private val formElementMapper: FormElementMapper) {
name = applicationForm.name,
formElements = applicationForm.formElements.map { formElementMapper.toFormElement(it) }.toMutableList(),
isTemplate = applicationForm.isTemplate,
- createdBy = applicationForm.createdBy,
- lastModifiedBy = applicationForm.lastModifiedBy,
+ createdBy = userMapper.toUser(applicationForm.createdBy),
+ lastModifiedBy = userMapper.toUser(applicationForm.lastModifiedBy),
createdAt = applicationForm.createdAt,
modifiedAt = applicationForm.modifiedAt
)
}
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(
name = createApplicationFormDto.name,
isTemplate = createApplicationFormDto.isTemplate,
- createdBy = createApplicationFormDto.createdBy,
- lastModifiedBy = createApplicationFormDto.lastModifiedBy
+ createdBy = createdBy,
+ lastModifiedBy = lastModifiedBy,
)
applicationForm.formElements = createApplicationFormDto.formElements
.map { formElementMapper.toFormElement(it, applicationForm) }
diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/CustomJwtAuthenticationConverter.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/CustomJwtAuthenticationConverter.kt
new file mode 100644
index 0000000..90d437a
--- /dev/null
+++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/CustomJwtAuthenticationConverter.kt
@@ -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 {
+ override fun convert(jwt: Jwt): AbstractAuthenticationToken {
+ val authorities: Collection = emptyList()
+
+ val userId = jwt.getClaimAsString("id")
+ val username = jwt.getClaimAsString("name")
+ val principal = CustomJwtTokenPrincipal(userId, username)
+
+ return CustomJwtAuthentication(jwt, principal, authorities)
+ }
+}
diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt
index 5d51376..0d3bf6a 100644
--- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt
+++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt
@@ -15,7 +15,10 @@ import org.springframework.security.web.SecurityFilterChain
class SecurityConfig {
@Bean
- fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+ fun securityFilterChain(
+ http: HttpSecurity,
+ customJwtAuthenticationConverter: CustomJwtAuthenticationConverter
+ ): SecurityFilterChain {
http {
csrf { disable() }
authorizeHttpRequests {
@@ -24,7 +27,7 @@ class SecurityConfig {
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
- jwt { }
+ jwt { jwtAuthenticationConverter = customJwtAuthenticationConverter }
}
}
diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtAuthentication.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtAuthentication.kt
new file mode 100644
index 0000000..d46a5ef
--- /dev/null
+++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtAuthentication.kt
@@ -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
+) : JwtAuthenticationToken(
+ jwt, authorities, principal.id
+) {
+ override fun getPrincipal(): CustomJwtTokenPrincipal {
+ return principal
+ }
+}
diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtTokenPrincipal.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtTokenPrincipal.kt
new file mode 100644
index 0000000..e8f4ddb
--- /dev/null
+++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtTokenPrincipal.kt
@@ -0,0 +1,6 @@
+package com.betriebsratkanzlei.legalconsenthub.security
+
+data class CustomJwtTokenPrincipal(
+ val id: String? = null,
+ val name: String? = null
+)
diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/User.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/User.kt
new file mode 100644
index 0000000..8b0324c
--- /dev/null
+++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/User.kt
@@ -0,0 +1,9 @@
+package com.betriebsratkanzlei.legalconsenthub.user
+
+import jakarta.persistence.Embeddable
+
+@Embeddable
+class User(
+ var name: String,
+ var id: String
+)
diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserMapper.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserMapper.kt
new file mode 100644
index 0000000..f13c319
--- /dev/null
+++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserMapper.kt
@@ -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,
+ )
+ }
+}
diff --git a/legalconsenthub-backend/src/main/resources/application.yaml b/legalconsenthub-backend/src/main/resources/application.yaml
index e3b5fc0..ad80bae 100644
--- a/legalconsenthub-backend/src/main/resources/application.yaml
+++ b/legalconsenthub-backend/src/main/resources/application.yaml
@@ -22,14 +22,6 @@ spring:
order_inserts: 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:
enabled: true
drop-first: false
diff --git a/legalconsenthub-backend/src/main/resources/db/migrations/001-schema.sql b/legalconsenthub-backend/src/main/resources/db/migrations/001-schema.sql
index 0b732b2..8d6d553 100644
--- a/legalconsenthub-backend/src/main/resources/db/migrations/001-schema.sql
+++ b/legalconsenthub-backend/src/main/resources/db/migrations/001-schema.sql
@@ -1,12 +1,14 @@
create table application_form
(
- is_template boolean not null,
- created_at timestamp(6) not null,
- modified_at timestamp(6) not null,
- id uuid not null,
- created_by varchar(255) not null,
- last_modified_by varchar(255) not null,
- name varchar(255) not null,
+ is_template boolean not null,
+ created_at timestamp(6) not null,
+ modified_at timestamp(6) not null,
+ id uuid not null,
+ created_by_id 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,
primary key (id)
);
diff --git a/legalconsenthub/pages/application-forms/[id].vue b/legalconsenthub/pages/application-forms/[id].vue
index 576e306..97f7c05 100644
--- a/legalconsenthub/pages/application-forms/[id].vue
+++ b/legalconsenthub/pages/application-forms/[id].vue
@@ -62,7 +62,7 @@ const applicationForm = computed({
})
const isReadOnly = computed(() => {
- return applicationForm.value?.createdBy !== user.value?.name
+ return applicationForm.value?.createdBy.id !== user.value?.id
})
async function onSubmit() {
diff --git a/legalconsenthub/pages/index.vue b/legalconsenthub/pages/index.vue
index b95ddba..b6870ef 100644
--- a/legalconsenthub/pages/index.vue
+++ b/legalconsenthub/pages/index.vue
@@ -31,11 +31,11 @@
#{{ index }} {{ applicationFormElem.name }}
- Zuletzt bearbeitet von {{ applicationFormElem.lastModifiedBy }} am
+ Zuletzt bearbeitet von {{ applicationFormElem.lastModifiedBy.name }} am
{{ formatDate(applicationFormElem.modifiedAt) }}
- Erstellt von {{ applicationFormElem.createdBy }} am {{ formatDate(applicationFormElem.createdAt) }}
+ Erstellt von {{ applicationFormElem.createdBy.name }} am {{ formatDate(applicationFormElem.createdAt) }}
diff --git a/legalconsenthub/server/utils/auth.ts b/legalconsenthub/server/utils/auth.ts
index 4035891..d75e501 100644
--- a/legalconsenthub/server/utils/auth.ts
+++ b/legalconsenthub/server/utils/auth.ts
@@ -9,7 +9,8 @@ export const auth = betterAuth({
plugins: [
jwt({
jwt: {
- issuer: 'http://192.168.178.105:3001'
+ issuer: 'http://192.168.178.105:3001',
+ expirationTime: '48h'
},
jwks: {
keyPairConfig: {