Skip to content

Commit 5a5c599

Browse files
committed
feat: manual access grants
1 parent d30c069 commit 5a5c599

7 files changed

Lines changed: 213 additions & 13 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { z } from 'zod'
2+
import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js'
3+
import prisma from '../../prisma.js'
4+
5+
export const createAccessForGliederungProcedure = defineProtectedMutateProcedure({
6+
key: 'createForGliederung',
7+
roleIds: ['ADMIN'],
8+
inputSchema: z.strictObject({
9+
accountId: z.string().uuid(),
10+
gliederungId: z.string().uuid(),
11+
}),
12+
handler: async ({ ctx, input }) => {
13+
await prisma.$transaction(async (tx) => {
14+
const record = await tx.gliederungToAccount.create({
15+
data: {
16+
createdAt: new Date(),
17+
confirmedAt: new Date(),
18+
confirmedByGliederung: true,
19+
role: 'DELEGATIONSLEITER',
20+
gliederungId: input.gliederungId,
21+
accountId: input.accountId,
22+
},
23+
select: {
24+
id: true,
25+
account: {
26+
select: {
27+
email: true,
28+
person: {
29+
select: {
30+
firstname: true,
31+
lastname: true,
32+
},
33+
},
34+
},
35+
},
36+
gliederung: {
37+
select: {
38+
name: true,
39+
},
40+
},
41+
},
42+
})
43+
44+
const description = `
45+
Der Account von ${record.account.person.firstname} ${record.account.person.lastname} (${record.account.email}) hat
46+
jetzt Zugriff auf die Gliederung ${record.gliederung.name}
47+
`
48+
await tx.activity.create({
49+
data: {
50+
type: 'CREATE',
51+
subjectType: 'gliederungtoaccount',
52+
subjectId: `${record.id}`,
53+
causerId: ctx.accountId,
54+
createdAt: new Date(),
55+
description,
56+
},
57+
})
58+
})
59+
},
60+
})

apps/api/src/services/access/access.router.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { mergeRouters } from '../../trpc.js'
2+
import { createAccessForGliederungProcedure } from './access.createForGliederung.js'
23
import { requestGliederungAccessCreateProcedure } from './access.requestGliederungCreate.js'
34
import { requestGliederungAdminDecideProcedure } from './access.requestGliederungPatch.js'
45
import { requestGliederungAccessValidateProcedure } from './access.requestGliederungValidate.js'
@@ -10,5 +11,6 @@ export const accessRouter = mergeRouters(
1011
listAllGliederungAdminRequestsProcedure,
1112
requestGliederungAccessCreateProcedure,
1213
requestGliederungAdminDecideProcedure,
13-
requestGliederungAccessValidateProcedure
14+
requestGliederungAccessValidateProcedure,
15+
createAccessForGliederungProcedure
1416
)

apps/frontend/src/components/LayoutComponents/Sidebar/Sidebar.vue

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import SidebarItems, { type DividerItem, type SidebarItem } from './SidebarItems
2121
import UserLogo from '@/components/UIComponents/UserLogo.vue'
2222
import { loggedInAccount, logout } from '@/composables/useAuthentication'
2323
import { useAssets } from '@/composables/useAssets'
24+
import Badge from '@/components/UIComponents/Badge.vue'
25+
import { roleMapping } from '@codeanker/api'
26+
import { roleColors } from '@/helpers/constants'
2427
2528
const route = useRoute()
2629
const { logoSmall } = useAssets()
@@ -36,8 +39,10 @@ const veranstaltungId = computed(() => {
3639
return undefined
3740
})
3841
39-
const hasPermissionToView = (permission) => {
40-
return permission.includes(loggedInAccount.value?.role)
42+
const role = computed(() => loggedInAccount.value?.role ?? 'USER')
43+
44+
const hasPermissionToView = (permissions: string[]) => {
45+
return permissions.includes(role.value)
4146
}
4247
4348
const navigation = computed<Array<SidebarItem | DividerItem>>(() => [
@@ -181,7 +186,7 @@ const navigation = computed<Array<SidebarItem | DividerItem>>(() => [
181186
},
182187
{
183188
type: 'SidebarItem',
184-
name: 'Gliederungsadmins',
189+
name: 'Berechtigungen',
185190
route: { name: 'Verwaltung Alle Zugriffsanfragen' },
186191
icon: LockOpenIcon,
187192
visible: hasPermissionToView(['ADMIN']),
@@ -257,5 +262,11 @@ const navigation = computed<Array<SidebarItem | DividerItem>>(() => [
257262
<ArrowRightOnRectangleIcon class="h-5 aspect-square" />
258263
</button>
259264
</div>
265+
266+
<Badge
267+
:text="roleMapping[role].human"
268+
:color="roleColors[role]"
269+
class="text-xs mt-4 w-fit mx-auto"
270+
/>
260271
</div>
261272
</template>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<script setup lang="ts">
2+
import { apiClient } from '@/api'
3+
import BasicTypeahead from '@/components/BasicInputs/BasicTypeahead.vue'
4+
import Button from '@/components/UIComponents/Button.vue'
5+
import Loading from '@/components/UIComponents/Loading.vue'
6+
import { ValidateForm } from '@codeanker/validation'
7+
import { XMarkIcon } from '@heroicons/vue/24/outline'
8+
import { useMutation, useQueryClient } from '@tanstack/vue-query'
9+
import { ref } from 'vue'
10+
11+
const input = ref<{
12+
account?: {
13+
id: string
14+
}
15+
gliederung?: {
16+
id: string
17+
}
18+
}>({})
19+
20+
async function queryObjectGliederungen(searchTerm) {
21+
return apiClient.gliederung.publicList.query({
22+
filter: { name: searchTerm },
23+
orderBy: [],
24+
pagination: { take: 100, skip: 0 },
25+
})
26+
}
27+
async function queryObjectAccount(searchTerm) {
28+
return apiClient.account.verwaltungList.query({
29+
filter: { email: searchTerm },
30+
})
31+
}
32+
33+
const queryClient = useQueryClient()
34+
35+
const { mutate, error, isError, isPending } = useMutation({
36+
mutationKey: ['createGliederungAccess'],
37+
mutationFn: async () => {
38+
if (!input.value.account || !input.value.gliederung) {
39+
throw new Error('form not valid')
40+
}
41+
42+
await apiClient.access.createForGliederung.mutate({
43+
accountId: input.value.account?.id,
44+
gliederungId: input.value.gliederung?.id,
45+
})
46+
47+
queryClient.invalidateQueries({
48+
queryKey: ['listAllGliederungAdminRequests'],
49+
})
50+
},
51+
})
52+
</script>
53+
54+
<template>
55+
<ValidateForm
56+
class="grid grid-cols-2 gap-4"
57+
@submit="mutate"
58+
>
59+
<BasicTypeahead
60+
v-model="input.gliederung"
61+
:query="queryObjectGliederungen"
62+
:input-formatter="(result) => result?.name"
63+
:result-formatter="(result) => result.name"
64+
:strict="true"
65+
:disabled="isPending"
66+
label="Gliederung"
67+
required
68+
placeholder="Gliederung suchen"
69+
/>
70+
<BasicTypeahead
71+
v-model="input.account"
72+
:query="queryObjectAccount"
73+
:input-formatter="(result) => result?.email"
74+
:result-formatter="(result) => result.email"
75+
:strict="true"
76+
:disabled="isPending"
77+
label="Account (E-Mail Adresse)"
78+
required
79+
placeholder="Account suchen"
80+
/>
81+
82+
<div class="mt-4 flex gap-4 items-center">
83+
<Button color="danger"> Abbrechen </Button>
84+
<Button
85+
color="primary"
86+
type="submit"
87+
>
88+
<template v-if="isPending">
89+
<Loading color="white" />
90+
</template>
91+
<span>Speichern</span>
92+
</Button>
93+
</div>
94+
</ValidateForm>
95+
96+
<template v-if="isError">
97+
<div class="flex flex-col text-center items-center justify-center text-danger-600">
98+
<XMarkIcon class="h-10 mb-2" />
99+
<span> {{ error }} </span>
100+
</div>
101+
</template>
102+
</template>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { Role } from '@codeanker/api'
2+
import type { StatusColors } from './getAccountStatusColors'
3+
4+
export const roleColors: Record<Role, StatusColors> = {
5+
ADMIN: 'danger',
6+
GLIEDERUNG_ADMIN: 'warning',
7+
USER: 'muted',
8+
}

apps/frontend/src/views/Verwaltung/AccessControl/GliederungAccessRequests.vue

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
<script setup lang="ts">
22
import { apiClient } from '@/api'
33
import BasicSwitch from '@/components/BasicInputs/BasicSwitch.vue'
4+
import FormGliederungAccess from '@/components/forms/access/FormGliederungAccess.vue'
45
import type { Query } from '@/components/Table/DataTable.vue'
56
import DataTable from '@/components/Table/DataTable.vue'
67
import initialData from '@/components/Table/initialData'
78
import Button from '@/components/UIComponents/Button.vue'
9+
import Modal from '@/components/UIComponents/Modal.vue'
810
import type { RouterInput, RouterOutput } from '@codeanker/api'
911
import { dayjs } from '@codeanker/helpers'
10-
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/outline'
12+
import { ArrowTopRightOnSquareIcon, PlusIcon } from '@heroicons/vue/24/outline'
1113
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
1214
import { createColumnHelper } from '@tanstack/vue-table'
13-
import { h, ref } from 'vue'
15+
import { h, ref, useTemplateRef } from 'vue'
1416
import { RouterLink } from 'vue-router'
1517
1618
type AccessRequest = RouterOutput['access']['listAllGliederungAdminRequests']['data'][number]
@@ -160,14 +162,23 @@ const query: Query<AccessRequest> = (pagination, filter, orderBy) =>
160162
initialData,
161163
placeholderData: keepPreviousData,
162164
})
165+
166+
const modalAdd = useTemplateRef('modalAdd')
163167
</script>
164168

165169
<template>
166170
<div class="my-4 flex items-center justify-between">
167171
<div>
168-
<p class="text-xl font-bold">Anfragen</p>
169-
<p class="text-sm">Hier findest du alle Zugriffsanfragen auf Gliederungen.</p>
172+
<p class="text-xl font-bold">Gliederungsadmin</p>
173+
<p class="text-sm">Hier findest du alle Accounts mit Zugriff auf Gliederungen.</p>
170174
</div>
175+
<span
176+
class="text-primary-500 flex items-center cursor-pointer"
177+
@click="() => modalAdd?.show()"
178+
>
179+
<PlusIcon class="h-5 w-5 mr-1" />
180+
<span>Berechtigung vergeben</span>
181+
</span>
171182
</div>
172183

173184
<DataTable
@@ -182,4 +193,14 @@ const query: Query<AccessRequest> = (pagination, filter, orderBy) =>
182193
/>
183194
</template>
184195
</DataTable>
196+
197+
<Teleport to="body">
198+
<Modal ref="modalAdd">
199+
<template #content>
200+
<div class="text-xl font-bold mb-8">Berechtigung vergeben</div>
201+
202+
<FormGliederungAccess />
203+
</template>
204+
</Modal>
205+
</Teleport>
185206
</template>

apps/frontend/src/views/Verwaltung/Accounts/AccountList.vue

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,13 @@ import { keepPreviousData, useQuery } from '@tanstack/vue-query'
1414
import { createColumnHelper } from '@tanstack/vue-table'
1515
import { h } from 'vue'
1616
import { useRouter } from 'vue-router'
17+
import { roleColors } from '@/helpers/constants'
1718
1819
const { setTitle } = useRouteTitle()
1920
setTitle('Accounts')
2021
2122
type Account = RouterOutput['account']['verwaltungList']['data'][number]
2223
23-
const roleColors: Record<Role, StatusColors> = {
24-
ADMIN: 'danger',
25-
GLIEDERUNG_ADMIN: 'warning',
26-
USER: 'muted',
27-
}
2824
const statusColors: Record<AccountStatus, StatusColors> = {
2925
AKTIV: 'primary',
3026
DEAKTIVIERT: 'danger',

0 commit comments

Comments
 (0)