feat(frontend): Add roles
This commit is contained in:
@@ -1,26 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<UModal v-model:open="open" title="Invite Member" description="Invite a member to your organization">
|
<UModal v-model:open="open" title="Mitglied einladen" description="Laden Sie ein Mitglied zu Ihrer Organisation ein">
|
||||||
<UButton icon="i-lucide-mail-plus" variant="outline" size="sm" @click="open = true"> Invite Member </UButton>
|
<UButton
|
||||||
|
v-if="canInviteMembers"
|
||||||
|
icon="i-lucide-mail-plus"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="open = true"
|
||||||
|
>
|
||||||
|
Mitglied einladen
|
||||||
|
</UButton>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<UForm :state="form" class="space-y-4" @submit="handleSubmit">
|
<UForm :state="form" class="space-y-4" @submit="handleSubmit">
|
||||||
<UFormField label="Email" name="email">
|
<UFormField label="E-Mail" name="email">
|
||||||
<UInput v-model="form.email" type="email" placeholder="Email" class="w-full" />
|
<UInput v-model="form.email" type="email" placeholder="E-Mail" class="w-full" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<UFormField label="Role" name="role">
|
<UFormField label="Rolle" name="role">
|
||||||
<USelect
|
<USelect
|
||||||
v-model="form.role"
|
v-model="form.role"
|
||||||
:items="roleOptions"
|
:items="availableRoles"
|
||||||
placeholder="Select a role"
|
placeholder="Rolle auswählen"
|
||||||
value-key="value"
|
value-key="value"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
>
|
||||||
|
<template #option="{ option }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon :name="option.icon" :class="`text-${option.color}-500`" />
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">{{ option.label }}</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ option.description }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</USelect>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<UButton type="submit" :loading="loading">
|
<UButton type="submit" :loading="loading">
|
||||||
{{ loading ? 'Inviting...' : 'Invite' }}
|
{{ loading ? 'Einladen...' : 'Einladen' }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</UForm>
|
</UForm>
|
||||||
@@ -29,10 +47,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
import { ROLES, type LegalRole } from '~/server/utils/permissions'
|
||||||
organization: ActiveOrganization
|
|
||||||
}>()
|
|
||||||
|
|
||||||
|
const { canInviteMembers } = usePermissions()
|
||||||
const { inviteMember } = useBetterAuth()
|
const { inviteMember } = useBetterAuth()
|
||||||
|
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
@@ -40,26 +57,68 @@ const loading = ref(false)
|
|||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
email: '',
|
email: '',
|
||||||
role: 'member'
|
role: ROLES.EMPLOYEE as LegalRole
|
||||||
})
|
})
|
||||||
|
|
||||||
const roleOptions = [
|
const availableRoles = computed(() => {
|
||||||
{ label: 'Admin', value: 'admin' },
|
return Object.values(ROLES).map(role => {
|
||||||
{ label: 'Member', value: 'member' }
|
const roleInfo = getRoleInfo(role)
|
||||||
]
|
return {
|
||||||
|
label: roleInfo.name,
|
||||||
|
value: role,
|
||||||
|
description: roleInfo.description,
|
||||||
|
color: roleInfo.color,
|
||||||
|
icon: roleInfo.icon
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
watch(open, (val) => {
|
function getRoleInfo(role: LegalRole) {
|
||||||
|
const roleInfo = {
|
||||||
|
[ROLES.EMPLOYER]: {
|
||||||
|
name: 'Arbeitgeber',
|
||||||
|
description: 'Kann Anträge genehmigen und Vereinbarungen unterzeichnen',
|
||||||
|
color: 'blue',
|
||||||
|
icon: 'i-lucide-briefcase'
|
||||||
|
},
|
||||||
|
[ROLES.EMPLOYEE]: {
|
||||||
|
name: 'Arbeitnehmer',
|
||||||
|
description: 'Kann eigene Anträge einsehen und kommentieren',
|
||||||
|
color: 'green',
|
||||||
|
icon: 'i-lucide-user'
|
||||||
|
},
|
||||||
|
[ROLES.WORKS_COUNCIL_MEMBER]: {
|
||||||
|
name: 'Betriebsrat',
|
||||||
|
description: 'Kann Anträge prüfen und Vereinbarungen unterzeichnen',
|
||||||
|
color: 'purple',
|
||||||
|
icon: 'i-lucide-users'
|
||||||
|
},
|
||||||
|
[ROLES.ADMIN]: {
|
||||||
|
name: 'Administrator',
|
||||||
|
description: 'Vollzugriff auf Organisationsverwaltung',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-lucide-settings'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roleInfo[role] || { name: role, description: '', color: 'gray', icon: 'i-lucide-circle' }
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(open, (val: boolean) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
form.value = { email: '', role: 'member' }
|
form.value = {
|
||||||
|
email: '',
|
||||||
|
role: ROLES.EMPLOYEE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
await inviteMember(form.value.email, form.value.role as 'member' | 'admin')
|
await inviteMember(form.value.email, form.value.role)
|
||||||
open.value = false
|
open.value = false
|
||||||
useToast().add({ title: 'Invitation sent', color: 'success' })
|
useToast().add({ title: 'Einladung gesendet', color: 'success' })
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,21 @@ import { organizationClient, jwtClient } from 'better-auth/client/plugins'
|
|||||||
import type { RouteLocationRaw } from 'vue-router'
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
import type { UserDto } from '~/.api-client'
|
import type { UserDto } from '~/.api-client'
|
||||||
import type { RouteLocationNormalizedLoaded } from '#vue-router'
|
import type { RouteLocationNormalizedLoaded } from '#vue-router'
|
||||||
|
import {
|
||||||
|
accessControl,
|
||||||
|
employerRole,
|
||||||
|
worksCouncilMemberRole,
|
||||||
|
employeeRole,
|
||||||
|
adminRole,
|
||||||
|
ROLES
|
||||||
|
} from '~/server/utils/permissions'
|
||||||
|
|
||||||
interface RuntimeAuthConfig {
|
interface RuntimeAuthConfig {
|
||||||
redirectUserTo: RouteLocationRaw | string
|
redirectUserTo: RouteLocationRaw | string
|
||||||
redirectGuestTo: RouteLocationRaw | string
|
redirectGuestTo: RouteLocationRaw | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Move into pinia store
|
||||||
const session = ref<InferSessionFromClient<ClientOptions> | null>(null)
|
const session = ref<InferSessionFromClient<ClientOptions> | null>(null)
|
||||||
const user = ref<InferUserFromClient<ClientOptions> | null>(null)
|
const user = ref<InferUserFromClient<ClientOptions> | null>(null)
|
||||||
const sessionFetching = import.meta.server ? ref(false) : ref(false)
|
const sessionFetching = import.meta.server ? ref(false) : ref(false)
|
||||||
@@ -28,6 +37,7 @@ const selectedOrganization = ref<{
|
|||||||
metadata?: any
|
metadata?: any
|
||||||
logo?: string | null
|
logo?: string | null
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
const activeMember = ref<{role: string} | null>(null)
|
||||||
|
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const url = useRequestURL()
|
const url = useRequestURL()
|
||||||
@@ -39,7 +49,19 @@ export function useAuth() {
|
|||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
headers
|
headers
|
||||||
},
|
},
|
||||||
plugins: [organizationClient(), jwtClient()]
|
plugins: [
|
||||||
|
organizationClient({
|
||||||
|
// Pass the same access control instance and roles to client
|
||||||
|
ac: accessControl,
|
||||||
|
roles: {
|
||||||
|
[ROLES.EMPLOYER]: employerRole,
|
||||||
|
[ROLES.WORKS_COUNCIL_MEMBER]: worksCouncilMemberRole,
|
||||||
|
[ROLES.EMPLOYEE]: employeeRole,
|
||||||
|
[ROLES.ADMIN]: adminRole
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
jwtClient()
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const options = defu(useRuntimeConfig().public.auth as Partial<RuntimeAuthConfig>, {
|
const options = defu(useRuntimeConfig().public.auth as Partial<RuntimeAuthConfig>, {
|
||||||
@@ -86,6 +108,14 @@ export function useAuth() {
|
|||||||
if (!selectedOrganization.value && organizations.value.length > 0) {
|
if (!selectedOrganization.value && organizations.value.length > 0) {
|
||||||
selectedOrganization.value = organizations.value[0]
|
selectedOrganization.value = organizations.value[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch active member
|
||||||
|
const activeMemberResult = await client.organization.getActiveMember({
|
||||||
|
fetchOptions: {
|
||||||
|
headers
|
||||||
|
}
|
||||||
|
})
|
||||||
|
activeMember.value = activeMemberResult.data || null
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -147,6 +177,7 @@ export function useAuth() {
|
|||||||
fetchJwtAndOrganizations,
|
fetchJwtAndOrganizations,
|
||||||
client,
|
client,
|
||||||
jwt,
|
jwt,
|
||||||
isPublicRoute
|
isPublicRoute,
|
||||||
|
activeMember
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import type { ActiveOrganization } from '~/types/betterAuth'
|
||||||
|
import type { LegalRole } from '~/server/utils/permissions'
|
||||||
|
|
||||||
const activeOrganization = ref<ActiveOrganization | null>(null)
|
const activeOrganization = ref<ActiveOrganization | null>(null)
|
||||||
const selectedOrgId = ref<string | undefined>(undefined)
|
const selectedOrgId = ref<string | undefined>(undefined)
|
||||||
|
|
||||||
@@ -7,7 +10,7 @@ export function useBetterAuth() {
|
|||||||
|
|
||||||
async function createOrganization(name: string, slug: string, logo?: string) {
|
async function createOrganization(name: string, slug: string, logo?: string) {
|
||||||
const slugCheck = await organization.checkSlug({ slug })
|
const slugCheck = await organization.checkSlug({ slug })
|
||||||
if (!slugCheck.data.available) {
|
if (!slugCheck.data?.status) {
|
||||||
toast.add({ title: 'Slug bereits vergeben', description: 'Bitte wählen Sie einen anderen Slug', color: 'error' })
|
toast.add({ title: 'Slug bereits vergeben', description: 'Bitte wählen Sie einen anderen Slug', color: 'error' })
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
}
|
}
|
||||||
@@ -62,7 +65,7 @@ export function useBetterAuth() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function inviteMember(email: string, role: 'member' | 'admin') {
|
async function inviteMember(email: string, role: LegalRole) {
|
||||||
await organization.inviteMember({
|
await organization.inviteMember({
|
||||||
email,
|
email,
|
||||||
role,
|
role,
|
||||||
|
|||||||
102
legalconsenthub/composables/usePermissions.ts
Normal file
102
legalconsenthub/composables/usePermissions.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { ROLES, type LegalRole } from '~/server/utils/permissions'
|
||||||
|
|
||||||
|
export function usePermissions() {
|
||||||
|
const { organization, activeMember } = useAuth()
|
||||||
|
|
||||||
|
const currentRole = computed((): LegalRole | null => {
|
||||||
|
return (activeMember.value?.role as LegalRole) || null
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasPermission = (permissions: Record<string, string[]>): boolean => {
|
||||||
|
if (!currentRole.value) return false
|
||||||
|
|
||||||
|
return organization.checkRolePermission({
|
||||||
|
permissions,
|
||||||
|
role: currentRole.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific permission helpers
|
||||||
|
const canCreateApplicationForm = computed(() =>
|
||||||
|
hasPermission({ application_form: ["create"] })
|
||||||
|
)
|
||||||
|
|
||||||
|
const canApproveApplicationForm = computed(() =>
|
||||||
|
hasPermission({ application_form: ["approve"] })
|
||||||
|
)
|
||||||
|
|
||||||
|
const canSignAgreement = computed(() =>
|
||||||
|
hasPermission({ agreement: ["sign"] })
|
||||||
|
)
|
||||||
|
|
||||||
|
const canInviteMembers = computed(() =>
|
||||||
|
hasPermission({ member: ["invite"] })
|
||||||
|
)
|
||||||
|
|
||||||
|
const canManageOrganization = computed(() =>
|
||||||
|
hasPermission({ organization: ["manage_settings"] })
|
||||||
|
)
|
||||||
|
|
||||||
|
// Role checks
|
||||||
|
const isEmployer = computed(() => currentRole.value === ROLES.EMPLOYER)
|
||||||
|
const isEmployee = computed(() => currentRole.value === ROLES.EMPLOYEE)
|
||||||
|
const isWorksCouncilMember = computed(() => currentRole.value === ROLES.WORKS_COUNCIL_MEMBER)
|
||||||
|
const isAdmin = computed(() => currentRole.value === ROLES.ADMIN)
|
||||||
|
|
||||||
|
const getCurrentRoleInfo = () => {
|
||||||
|
const roleInfo = {
|
||||||
|
[ROLES.EMPLOYER]: {
|
||||||
|
name: 'Arbeitgeber',
|
||||||
|
description: 'Kann Anträge genehmigen und Vereinbarungen unterzeichnen',
|
||||||
|
color: 'blue',
|
||||||
|
icon: 'i-lucide-briefcase'
|
||||||
|
},
|
||||||
|
[ROLES.EMPLOYEE]: {
|
||||||
|
name: 'Arbeitnehmer',
|
||||||
|
description: 'Kann eigene Anträge einsehen und kommentieren',
|
||||||
|
color: 'green',
|
||||||
|
icon: 'i-lucide-user'
|
||||||
|
},
|
||||||
|
[ROLES.WORKS_COUNCIL_MEMBER]: {
|
||||||
|
name: 'Betriebsrat',
|
||||||
|
description: 'Kann Anträge prüfen und Vereinbarungen unterzeichnen',
|
||||||
|
color: 'purple',
|
||||||
|
icon: 'i-lucide-users'
|
||||||
|
},
|
||||||
|
[ROLES.ADMIN]: {
|
||||||
|
name: 'Administrator',
|
||||||
|
description: 'Vollzugriff auf Organisationsverwaltung',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-lucide-settings'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentRole.value && currentRole.value in roleInfo ? roleInfo[currentRole.value as LegalRole] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
currentRole,
|
||||||
|
activeMember,
|
||||||
|
|
||||||
|
// Permission checks
|
||||||
|
hasPermission,
|
||||||
|
|
||||||
|
// Role checks
|
||||||
|
isEmployer,
|
||||||
|
isEmployee,
|
||||||
|
isWorksCouncilMember,
|
||||||
|
isAdmin,
|
||||||
|
|
||||||
|
// Computed permissions
|
||||||
|
canCreateApplicationForm,
|
||||||
|
canApproveApplicationForm,
|
||||||
|
canSignAgreement,
|
||||||
|
canInviteMembers,
|
||||||
|
canManageOrganization,
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
getCurrentRoleInfo,
|
||||||
|
ROLES
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,10 +16,40 @@
|
|||||||
|
|
||||||
<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 }}
|
<!-- Permission Guard using Better Auth's native system -->
|
||||||
<UPageCard title="Ampelstatus" variant="naked" orientation="horizontal" class="mb-4">
|
<div v-if="!canCreateApplicationForm" class="text-center py-12">
|
||||||
{{ ampelStatusEmoji }}
|
<UIcon name="i-lucide-shield-x" class="w-16 h-16 mx-auto text-red-400 mb-4" />
|
||||||
</UPageCard>
|
<h2 class="text-2xl font-semibold text-gray-700 mb-2">Keine Berechtigung</h2>
|
||||||
|
<p class="text-gray-500 mb-4">
|
||||||
|
Sie haben keine Berechtigung zum Erstellen von Anträgen.
|
||||||
|
</p>
|
||||||
|
<UAlert
|
||||||
|
v-if="currentRoleInfo"
|
||||||
|
:title="`Ihre aktuelle Rolle: ${currentRoleInfo.name}`"
|
||||||
|
:description="currentRoleInfo.description"
|
||||||
|
:color="currentRoleInfo.color"
|
||||||
|
variant="soft"
|
||||||
|
class="max-w-md mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
Erstelle Formular für Organisation: {{ selectedOrganization?.name }}
|
||||||
|
|
||||||
|
<!-- Role Context Alert -->
|
||||||
|
<UAlert
|
||||||
|
v-if="currentRoleInfo"
|
||||||
|
:title="`Erstellen als: ${currentRoleInfo.name}`"
|
||||||
|
:description="`${currentRoleInfo.description} - Sie können Anträge erstellen und bearbeiten.`"
|
||||||
|
:color="currentRoleInfo.color"
|
||||||
|
variant="soft"
|
||||||
|
:icon="currentRoleInfo.icon"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UPageCard title="Ampelstatus" variant="naked" orientation="horizontal" class="mb-4">
|
||||||
|
{{ ampelStatusEmoji }}
|
||||||
|
</UPageCard>
|
||||||
|
|
||||||
<UPageCard variant="subtle">
|
<UPageCard variant="subtle">
|
||||||
<UForm class="space-y-4" :state="{}" @submit="onSubmit">
|
<UForm class="space-y-4" :state="{}" @submit="onSubmit">
|
||||||
@@ -55,6 +85,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</UForm>
|
</UForm>
|
||||||
</UPageCard>
|
</UPageCard>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
@@ -70,6 +101,10 @@ const { getAllApplicationFormTemplates } = useApplicationFormTemplate()
|
|||||||
const { createApplicationForm } = useApplicationForm()
|
const { createApplicationForm } = useApplicationForm()
|
||||||
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
|
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
|
||||||
const { userDto, selectedOrganization } = useAuth()
|
const { userDto, selectedOrganization } = useAuth()
|
||||||
|
const { canCreateApplicationForm, getCurrentRoleInfo } = usePermissions()
|
||||||
|
|
||||||
|
// Get current role information for display
|
||||||
|
const currentRoleInfo = computed(() => getCurrentRoleInfo())
|
||||||
|
|
||||||
const stepper = useTemplateRef('stepper')
|
const stepper = useTemplateRef('stepper')
|
||||||
const activeStepperItemIndex = ref<number>(0)
|
const activeStepperItemIndex = ref<number>(0)
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ import { betterAuth } from 'better-auth'
|
|||||||
import Database from 'better-sqlite3'
|
import Database from 'better-sqlite3'
|
||||||
import { organization, jwt } from 'better-auth/plugins'
|
import { organization, jwt } from 'better-auth/plugins'
|
||||||
import { resend } from './mail'
|
import { resend } from './mail'
|
||||||
|
import {
|
||||||
|
accessControl,
|
||||||
|
employerRole,
|
||||||
|
worksCouncilMemberRole,
|
||||||
|
employeeRole,
|
||||||
|
adminRole,
|
||||||
|
ROLES,
|
||||||
|
type LegalRole
|
||||||
|
} from './permissions'
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: new Database('./sqlite.db'),
|
database: new Database('./sqlite.db'),
|
||||||
@@ -21,16 +30,46 @@ export const auth = betterAuth({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
organization({
|
organization({
|
||||||
|
// Pass the access control instance and roles
|
||||||
|
ac: accessControl,
|
||||||
|
roles: {
|
||||||
|
[ROLES.EMPLOYER]: employerRole,
|
||||||
|
[ROLES.WORKS_COUNCIL_MEMBER]: worksCouncilMemberRole,
|
||||||
|
[ROLES.EMPLOYEE]: employeeRole,
|
||||||
|
[ROLES.ADMIN]: adminRole
|
||||||
|
},
|
||||||
|
|
||||||
|
// Creator gets admin role by default
|
||||||
|
creatorRole: ROLES.ADMIN,
|
||||||
|
|
||||||
async sendInvitationEmail(data) {
|
async sendInvitationEmail(data) {
|
||||||
console.log('Sending invitation email', data)
|
console.log('Sending invitation email', data)
|
||||||
const inviteLink = `http://192.168.178.114:3001/accept-invitation/${data.id}`
|
const inviteLink = `http://192.168.178.114:3001/accept-invitation/${data.id}`
|
||||||
|
|
||||||
|
const roleDisplayNames = {
|
||||||
|
[ROLES.EMPLOYER]: 'Arbeitgeber',
|
||||||
|
[ROLES.EMPLOYEE]: 'Arbeitnehmer',
|
||||||
|
[ROLES.WORKS_COUNCIL_MEMBER]: 'Betriebsrat',
|
||||||
|
[ROLES.ADMIN]: 'Administrator'
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleDisplayName = roleDisplayNames[data.role as LegalRole] || data.role
|
||||||
|
|
||||||
await resend.emails.send({
|
await resend.emails.send({
|
||||||
from: 'Acme <onboarding@resend.dev>',
|
from: 'Legal Consent Hub <noreply@legalconsenthub.com>',
|
||||||
to: data.email,
|
to: data.email,
|
||||||
subject: 'Email Verification',
|
subject: `Einladung als ${roleDisplayName} - ${data.organization.name}`,
|
||||||
html: `You are invited to the Organization ${data.organization.name}! Click the link to verify your email: ${inviteLink}`
|
html: `
|
||||||
|
<h2>Einladung zur Organisation ${data.organization.name}</h2>
|
||||||
|
<p>Sie wurden als <strong>${roleDisplayName}</strong> eingeladen.</p>
|
||||||
|
<p><a href="${inviteLink}" style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Einladung annehmen</a></p>
|
||||||
|
<p>Diese Einladung läuft ab am: ${new Date(data.invitation.expiresAt).toLocaleDateString('de-DE')}</p>
|
||||||
|
`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export { ROLES }
|
||||||
|
export type { LegalRole }
|
||||||
|
|||||||
55
legalconsenthub/server/utils/permissions.ts
Normal file
55
legalconsenthub/server/utils/permissions.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { createAccessControl } from 'better-auth/plugins/access'
|
||||||
|
|
||||||
|
export const statement = {
|
||||||
|
application_form: ['create', 'read', 'update', 'delete', 'approve', 'reject', 'submit'],
|
||||||
|
agreement: ['create', 'read', 'update', 'sign', 'approve', 'reject'],
|
||||||
|
organization: ['create', 'read', 'update', 'delete', 'manage_settings'],
|
||||||
|
member: ['create', 'read', 'update', 'delete', 'invite', 'remove'],
|
||||||
|
comment: ['create', 'read', 'update', 'delete'],
|
||||||
|
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const accessControl = createAccessControl(statement)
|
||||||
|
|
||||||
|
// Roles with specific permissions
|
||||||
|
export const employerRole = accessControl.newRole({
|
||||||
|
application_form: ['create', 'read', 'approve', 'reject'],
|
||||||
|
agreement: ['create', 'read', 'sign', 'approve'],
|
||||||
|
member: ['invite', 'read'],
|
||||||
|
comment: ['create', 'read', 'update', 'delete'],
|
||||||
|
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
|
||||||
|
})
|
||||||
|
|
||||||
|
export const worksCouncilMemberRole = accessControl.newRole({
|
||||||
|
application_form: ['create', 'read', 'update', 'submit'],
|
||||||
|
agreement: ['read', 'sign', 'approve'],
|
||||||
|
member: ['read'],
|
||||||
|
comment: ['create', 'read', 'update', 'delete'],
|
||||||
|
document: ['create', 'read', 'update', 'download', 'upload']
|
||||||
|
})
|
||||||
|
|
||||||
|
export const employeeRole = accessControl.newRole({
|
||||||
|
application_form: ['read'],
|
||||||
|
agreement: ['read'],
|
||||||
|
member: ['read'],
|
||||||
|
comment: ['create', 'read'],
|
||||||
|
document: ['read', 'download']
|
||||||
|
})
|
||||||
|
|
||||||
|
export const adminRole = accessControl.newRole({
|
||||||
|
application_form: ['create', 'read', 'update', 'delete', 'approve', 'reject'],
|
||||||
|
agreement: ['create', 'read', 'update', 'sign', 'approve', 'reject'],
|
||||||
|
organization: ['create', 'read', 'update', 'delete', 'manage_settings'],
|
||||||
|
member: ['create', 'read', 'update', 'delete', 'invite', 'remove'],
|
||||||
|
comment: ['create', 'read', 'update', 'delete'],
|
||||||
|
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ROLES = {
|
||||||
|
EMPLOYER: 'employer',
|
||||||
|
WORKS_COUNCIL_MEMBER: 'works_council_member',
|
||||||
|
EMPLOYEE: 'employee',
|
||||||
|
ADMIN: 'admin'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type LegalRole = (typeof ROLES)[keyof typeof ROLES]
|
||||||
3
legalconsenthub/types/betterAuth.ts
Normal file
3
legalconsenthub/types/betterAuth.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const { client } = useAuth()
|
||||||
|
|
||||||
|
export type ActiveOrganization = typeof client.$Infer.ActiveOrganization
|
||||||
Reference in New Issue
Block a user