feat(fullstack): Add organization scoping for application form
This commit is contained in:
@@ -20,6 +20,12 @@ paths:
|
|||||||
operationId: getAllApplicationForms
|
operationId: getAllApplicationForms
|
||||||
tags:
|
tags:
|
||||||
- application-form
|
- application-form
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: organizationId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Filter application forms by organization ID
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Paged list of application forms
|
description: Paged list of application forms
|
||||||
@@ -580,6 +586,7 @@ components:
|
|||||||
- name
|
- name
|
||||||
- formElements
|
- formElements
|
||||||
- isTemplate
|
- isTemplate
|
||||||
|
- organizationId
|
||||||
- createdBy
|
- createdBy
|
||||||
- lastModifiedBy
|
- lastModifiedBy
|
||||||
- createdAt
|
- createdAt
|
||||||
@@ -596,6 +603,8 @@ components:
|
|||||||
$ref: "#/components/schemas/FormElementDto"
|
$ref: "#/components/schemas/FormElementDto"
|
||||||
isTemplate:
|
isTemplate:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
organizationId:
|
||||||
|
type: string
|
||||||
createdBy:
|
createdBy:
|
||||||
$ref: "#/components/schemas/UserDto"
|
$ref: "#/components/schemas/UserDto"
|
||||||
lastModifiedBy:
|
lastModifiedBy:
|
||||||
@@ -612,8 +621,6 @@ components:
|
|||||||
- name
|
- name
|
||||||
- formElements
|
- formElements
|
||||||
- isTemplate
|
- isTemplate
|
||||||
- createdBy
|
|
||||||
- lastModifiedBy
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
@@ -625,6 +632,8 @@ components:
|
|||||||
isTemplate:
|
isTemplate:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
organizationId:
|
||||||
|
type: string
|
||||||
|
|
||||||
PagedApplicationFormDto:
|
PagedApplicationFormDto:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class ApplicationForm(
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var isTemplate: Boolean,
|
var isTemplate: Boolean,
|
||||||
|
|
||||||
|
var organizationId: String = "",
|
||||||
|
|
||||||
@Embedded
|
@Embedded
|
||||||
@AttributeOverrides(
|
@AttributeOverrides(
|
||||||
AttributeOverride(name = "id", column = Column(name = "created_by_id", nullable = false)),
|
AttributeOverride(name = "id", column = Column(name = "created_by_id", nullable = false)),
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ class ApplicationFormController(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAllApplicationForms(): ResponseEntity<PagedApplicationFormDto> {
|
override fun getAllApplicationForms(organizationId: String?): ResponseEntity<PagedApplicationFormDto> {
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
pagedApplicationFormMapper.toPagedApplicationFormDto(
|
pagedApplicationFormMapper.toPagedApplicationFormDto(
|
||||||
applicationFormService.getApplicationForms()
|
applicationFormService.getApplicationForms(organizationId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class ApplicationFormMapper(private val formElementMapper: FormElementMapper, pr
|
|||||||
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,
|
||||||
|
organizationId = applicationForm.organizationId,
|
||||||
createdBy = userMapper.toUserDto(applicationForm.createdBy),
|
createdBy = userMapper.toUserDto(applicationForm.createdBy),
|
||||||
lastModifiedBy = userMapper.toUserDto(applicationForm.lastModifiedBy),
|
lastModifiedBy = userMapper.toUserDto(applicationForm.lastModifiedBy),
|
||||||
createdAt = applicationForm.createdAt ?: LocalDateTime.now(),
|
createdAt = applicationForm.createdAt ?: LocalDateTime.now(),
|
||||||
@@ -30,6 +31,7 @@ class ApplicationFormMapper(private val formElementMapper: FormElementMapper, pr
|
|||||||
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,
|
||||||
|
organizationId = applicationForm.organizationId,
|
||||||
createdBy = userMapper.toUser(applicationForm.createdBy),
|
createdBy = userMapper.toUser(applicationForm.createdBy),
|
||||||
lastModifiedBy = userMapper.toUser(applicationForm.lastModifiedBy),
|
lastModifiedBy = userMapper.toUser(applicationForm.lastModifiedBy),
|
||||||
createdAt = applicationForm.createdAt,
|
createdAt = applicationForm.createdAt,
|
||||||
@@ -38,6 +40,7 @@ class ApplicationFormMapper(private val formElementMapper: FormElementMapper, pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun toApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
|
fun toApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
|
||||||
|
// TODO: Move this in upper layer
|
||||||
val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
|
val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
|
||||||
val createdBy = User(principal.name ?: "UNKNOWN USER", principal.id ?: "")
|
val createdBy = User(principal.name ?: "UNKNOWN USER", principal.id ?: "")
|
||||||
val lastModifiedBy = User(principal.name ?: "UNKNOWN USER", principal.id ?: "")
|
val lastModifiedBy = User(principal.name ?: "UNKNOWN USER", principal.id ?: "")
|
||||||
@@ -45,6 +48,7 @@ class ApplicationFormMapper(private val formElementMapper: FormElementMapper, pr
|
|||||||
val applicationForm = ApplicationForm(
|
val applicationForm = ApplicationForm(
|
||||||
name = createApplicationFormDto.name,
|
name = createApplicationFormDto.name,
|
||||||
isTemplate = createApplicationFormDto.isTemplate,
|
isTemplate = createApplicationFormDto.isTemplate,
|
||||||
|
organizationId = createApplicationFormDto.organizationId ?: "",
|
||||||
createdBy = createdBy,
|
createdBy = createdBy,
|
||||||
lastModifiedBy = lastModifiedBy,
|
lastModifiedBy = lastModifiedBy,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package com.betriebsratkanzlei.legalconsenthub.application_form
|
|||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.data.jpa.repository.Query
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
interface ApplicationFormRepository : JpaRepository<ApplicationForm, UUID> {
|
interface ApplicationFormRepository : JpaRepository<ApplicationForm, UUID> {
|
||||||
fun findAllByIsTemplateTrue(page: Pageable): Page<ApplicationForm>
|
fun findAllByIsTemplateTrue(page: Pageable): Page<ApplicationForm>
|
||||||
fun findAllByIsTemplateFalse(page: Pageable): Page<ApplicationForm>
|
|
||||||
|
@Query("SELECT c FROM ApplicationForm c WHERE (c.isTemplate IS false) AND (:organizationId is null or c.organizationId = :organizationId)")
|
||||||
|
fun findAllByIsTemplateFalseAndOrganizationId(organizationId: String?, page: Pageable): Page<ApplicationForm>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,13 @@ class ApplicationFormService(
|
|||||||
return applicationFormRepository.findById(id).orElseThrow { ApplicationFormNotFoundException(id) }
|
return applicationFormRepository.findById(id).orElseThrow { ApplicationFormNotFoundException(id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApplicationForms(): Page<ApplicationForm> {
|
fun getApplicationForms(organizationId: String?): Page<ApplicationForm> {
|
||||||
val pageable = PageRequest.of(0, 10)
|
val pageable = PageRequest.of(0, 10)
|
||||||
return applicationFormRepository.findAllByIsTemplateFalse(pageable)
|
return applicationFormRepository.findAllByIsTemplateFalseAndOrganizationId(organizationId, pageable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateApplicationForm(applicationFormDto: ApplicationFormDto): ApplicationForm {
|
fun updateApplicationForm(applicationFormDto: ApplicationFormDto): ApplicationForm {
|
||||||
|
// TODO find statt mappen?
|
||||||
val applicationForm = applicationFormMapper.toApplicationForm(applicationFormDto)
|
val applicationForm = applicationFormMapper.toApplicationForm(applicationFormDto)
|
||||||
val updatedApplicationForm: ApplicationForm
|
val updatedApplicationForm: ApplicationForm
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ create table application_form
|
|||||||
last_modified_by_id varchar(255) not null,
|
last_modified_by_id varchar(255) not null,
|
||||||
last_modified_by_name varchar(255) not null,
|
last_modified_by_name varchar(255) not null,
|
||||||
name varchar(255) not null,
|
name varchar(255) not null,
|
||||||
|
organization_id varchar(255),
|
||||||
primary key (id)
|
primary key (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ export function useApplicationForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAllApplicationForms(): Promise<PagedApplicationFormDto> {
|
async function getAllApplicationForms(organizationId: string): Promise<PagedApplicationFormDto> {
|
||||||
try {
|
try {
|
||||||
return await applicationFormApi.getAllApplicationForms()
|
return await applicationFormApi.getAllApplicationForms(organizationId)
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof ResponseError) {
|
if (e instanceof ResponseError) {
|
||||||
console.error('Failed retrieving application forms:', e.response)
|
console.error('Failed retrieving application forms:', e.response)
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export function useApplicationFormApi() {
|
|||||||
return applicationFormApiClient.createApplicationForm({ createApplicationFormDto })
|
return applicationFormApiClient.createApplicationForm({ createApplicationFormDto })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAllApplicationForms(): Promise<PagedApplicationFormDto> {
|
async function getAllApplicationForms(organizationId: string): Promise<PagedApplicationFormDto> {
|
||||||
return applicationFormApiClient.getAllApplicationForms()
|
return applicationFormApiClient.getAllApplicationForms({ organizationId })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getApplicationFormById(id: string): Promise<ApplicationFormDto> {
|
async function getApplicationFormById(id: string): Promise<ApplicationFormDto> {
|
||||||
|
|||||||
@@ -11,6 +11,22 @@ interface RuntimeAuthConfig {
|
|||||||
redirectGuestTo: RouteLocationRaw | string
|
redirectGuestTo: RouteLocationRaw | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const session = ref<InferSessionFromClient<ClientOptions> | null>(null)
|
||||||
|
const user = ref<InferUserFromClient<ClientOptions> | null>(null)
|
||||||
|
const sessionFetching = import.meta.server ? ref(false) : ref(false)
|
||||||
|
const jwt = ref<string | null>(null)
|
||||||
|
const organizations = ref<
|
||||||
|
{ id: string; name: string; createdAt: Date; slug: string; metadata?: any; logo?: string | null }[]
|
||||||
|
>([])
|
||||||
|
const selectedOrganization = ref<{
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
createdAt: Date
|
||||||
|
slug: string
|
||||||
|
metadata?: any
|
||||||
|
logo?: string | null
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const url = useRequestURL()
|
const url = useRequestURL()
|
||||||
const headers = import.meta.server ? useRequestHeaders() : undefined
|
const headers = import.meta.server ? useRequestHeaders() : undefined
|
||||||
@@ -27,10 +43,6 @@ export function useAuth() {
|
|||||||
redirectUserTo: '/',
|
redirectUserTo: '/',
|
||||||
redirectGuestTo: '/login'
|
redirectGuestTo: '/login'
|
||||||
})
|
})
|
||||||
const session = useState<InferSessionFromClient<ClientOptions> | null>('auth:session', () => null)
|
|
||||||
const user = useState<InferUserFromClient<ClientOptions> | null>('auth:user', () => null)
|
|
||||||
const sessionFetching = import.meta.server ? ref(false) : useState('auth:sessionFetching', () => false)
|
|
||||||
const jwt = useState<string | null>('auth:jwt', () => null)
|
|
||||||
|
|
||||||
async function fetchSession() {
|
async function fetchSession() {
|
||||||
if (sessionFetching.value) {
|
if (sessionFetching.value) {
|
||||||
@@ -43,13 +55,33 @@ export function useAuth() {
|
|||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
jwt.value = (await client.token()).data?.token ?? null
|
|
||||||
session.value = data?.session || null
|
session.value = data?.session || null
|
||||||
user.value = data?.user || null
|
user.value = data?.user || null
|
||||||
sessionFetching.value = false
|
sessionFetching.value = false
|
||||||
|
|
||||||
|
// Fetch JWT - workaround for not working extraction of JWT out of session (https://github.com/better-auth/better-auth/issues/1835)
|
||||||
|
jwt.value = (await client.token()).data?.token ?? null
|
||||||
|
|
||||||
|
// Fetch organization
|
||||||
|
organizations.value = (await client.organization.list()).data ?? []
|
||||||
|
if (!selectedOrganization.value && organizations.value.length > 0) {
|
||||||
|
selectedOrganization.value = organizations.value[0]
|
||||||
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedOrganization.value,
|
||||||
|
async (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
await client.organization.setActive({
|
||||||
|
organizationId: newValue?.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
client.$store.listen('$sessionSignal', async (signal) => {
|
client.$store.listen('$sessionSignal', async (signal) => {
|
||||||
if (!signal) return
|
if (!signal) return
|
||||||
@@ -79,6 +111,8 @@ export function useAuth() {
|
|||||||
signUp: client.signUp,
|
signUp: client.signUp,
|
||||||
signOut,
|
signOut,
|
||||||
organization: client.organization,
|
organization: client.organization,
|
||||||
|
organizations,
|
||||||
|
selectedOrganization,
|
||||||
options,
|
options,
|
||||||
fetchSession,
|
fetchSession,
|
||||||
client,
|
client,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col gap-4 sm:gap-6 lg:gap-12 w-full lg:max-w-4xl mx-auto">
|
<div class="flex flex-col gap-4 sm:gap-6 lg:gap-12 w-full lg:max-w-4xl mx-auto">
|
||||||
|
Erstelle Forumular für Organisation: {{ selectedOrganization?.name }}
|
||||||
<UPageCard title="Ampelstatus" variant="naked" orientation="horizontal" class="mb-4">
|
<UPageCard title="Ampelstatus" variant="naked" orientation="horizontal" class="mb-4">
|
||||||
{{ ampelStatusEmoji }}
|
{{ ampelStatusEmoji }}
|
||||||
</UPageCard>
|
</UPageCard>
|
||||||
@@ -35,14 +36,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ComplianceStatus, type PagedApplicationFormDto } from '~/.api-client'
|
import { ComplianceStatus, type PagedApplicationFormDto, type UserDto } from '~/.api-client'
|
||||||
import { useApplicationFormValidator } from '~/composables/useApplicationFormValidator'
|
import { useApplicationFormValidator } from '~/composables/useApplicationFormValidator'
|
||||||
import type { FormElementId } from '~/types/FormElement'
|
import type { FormElementId } from '~/types/FormElement'
|
||||||
|
|
||||||
const { getAllApplicationFormTemplates } = useApplicationFormTemplate()
|
const { getAllApplicationFormTemplates } = useApplicationFormTemplate()
|
||||||
const { createApplicationForm } = useApplicationForm()
|
const { createApplicationForm } = useApplicationForm()
|
||||||
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
|
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
|
||||||
const { user } = await useAuth()
|
const { user, selectedOrganization } = useAuth()
|
||||||
|
|
||||||
const { data } = await useAsyncData<PagedApplicationFormDto>(async () => {
|
const { data } = await useAsyncData<PagedApplicationFormDto>(async () => {
|
||||||
return await getAllApplicationFormTemplates()
|
return await getAllApplicationFormTemplates()
|
||||||
@@ -92,8 +93,13 @@ const ampelStatusEmoji = computed(() => {
|
|||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
if (applicationFormTemplate.value) {
|
if (applicationFormTemplate.value) {
|
||||||
applicationFormTemplate.value.createdBy = user.value?.name ?? 'Unknown'
|
const userDto: UserDto = {
|
||||||
applicationFormTemplate.value.lastModifiedBy = user.value?.name ?? 'Unknown'
|
id: user.value?.id ?? '',
|
||||||
|
name: user.value?.name ?? 'Unknown'
|
||||||
|
}
|
||||||
|
applicationFormTemplate.value.createdBy = userDto
|
||||||
|
applicationFormTemplate.value.lastModifiedBy = userDto
|
||||||
|
applicationFormTemplate.value.organizationId = selectedOrganization.value?.id ?? ''
|
||||||
|
|
||||||
await createApplicationForm(applicationFormTemplate.value)
|
await createApplicationForm(applicationFormTemplate.value)
|
||||||
await navigateTo('/')
|
await navigateTo('/')
|
||||||
|
|||||||
@@ -7,6 +7,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #right>
|
<template #right>
|
||||||
|
Aktuelle Organisation
|
||||||
|
<USelect
|
||||||
|
v-model="selectedOrganizationId"
|
||||||
|
:items="organizations"
|
||||||
|
value-key="id"
|
||||||
|
label-key="name"
|
||||||
|
size="lg"
|
||||||
|
:ui="{
|
||||||
|
trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200'
|
||||||
|
}"
|
||||||
|
class="w-48"
|
||||||
|
/>
|
||||||
<UDropdownMenu :items="items">
|
<UDropdownMenu :items="items">
|
||||||
<UButton icon="i-lucide-plus" size="md" class="rounded-full" />
|
<UButton icon="i-lucide-plus" size="md" class="rounded-full" />
|
||||||
</UDropdownMenu>
|
</UDropdownMenu>
|
||||||
@@ -58,10 +70,17 @@ import type { ApplicationFormDto, PagedApplicationFormDto } from '~/.api-client'
|
|||||||
|
|
||||||
const { getAllApplicationForms, deleteApplicationFormById } = useApplicationForm()
|
const { getAllApplicationForms, deleteApplicationFormById } = useApplicationForm()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const { organizations, selectedOrganization } = useAuth()
|
||||||
|
|
||||||
const { data } = await useAsyncData<PagedApplicationFormDto>(async () => {
|
const { data } = await useAsyncData<PagedApplicationFormDto>(
|
||||||
return await getAllApplicationForms()
|
async () => {
|
||||||
})
|
if (!selectedOrganization.value) {
|
||||||
|
throw new Error('No organization selected')
|
||||||
|
}
|
||||||
|
return await getAllApplicationForms(selectedOrganization.value.id)
|
||||||
|
},
|
||||||
|
{ watch: [selectedOrganization] }
|
||||||
|
)
|
||||||
|
|
||||||
const isDeleteModalOpen = computed<boolean>({
|
const isDeleteModalOpen = computed<boolean>({
|
||||||
get: () => 'delete' in route.query,
|
get: () => 'delete' in route.query,
|
||||||
@@ -75,6 +94,16 @@ const applicationFormNameToDelete = computed(() => {
|
|||||||
return data?.value?.content.find((appForm) => appForm.id === route.query.id)
|
return data?.value?.content.find((appForm) => appForm.id === route.query.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedOrganizationId = computed({
|
||||||
|
get() {
|
||||||
|
return selectedOrganization.value?.id
|
||||||
|
},
|
||||||
|
set(item) {
|
||||||
|
// TODO: USelect triggers multiple times after single selection
|
||||||
|
selectedOrganization.value = organizations.value.find((i) => i.id === item) ?? null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user