feat: Implement permission management with dynamic access control for contacts
This commit is contained in:
parent
47b7099ba6
commit
684d0deaaa
@ -187,3 +187,9 @@ if (savedPrimary || savedSurface) {
|
|||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
authStore.initializeFromElement();
|
authStore.initializeFromElement();
|
||||||
|
|
||||||
|
// Load user permissions
|
||||||
|
import { usePermissionStore } from './js/stores/permissions';
|
||||||
|
const permissionStore = usePermissionStore();
|
||||||
|
if (authStore.isAuthenticated) {
|
||||||
|
permissionStore.loadPermissions();
|
||||||
|
}
|
||||||
|
|||||||
@ -88,6 +88,16 @@
|
|||||||
<slot name="actions" v-bind="slotProps">
|
<slot name="actions" v-bind="slotProps">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
v-if="showViewButton"
|
||||||
|
icon="pi pi-eye"
|
||||||
|
outlined
|
||||||
|
rounded
|
||||||
|
@click="$emit('view', slotProps.data)"
|
||||||
|
size="small"
|
||||||
|
severity="secondary"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="showEditButton"
|
||||||
icon="pi pi-pencil"
|
icon="pi pi-pencil"
|
||||||
outlined
|
outlined
|
||||||
rounded
|
rounded
|
||||||
@ -95,6 +105,7 @@
|
|||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
v-if="showDeleteButton"
|
||||||
icon="pi pi-trash"
|
icon="pi pi-trash"
|
||||||
outlined
|
outlined
|
||||||
rounded
|
rounded
|
||||||
@ -161,6 +172,18 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
showViewButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
showEditButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
showDeleteButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
emptyMessage: {
|
emptyMessage: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Keine Daten gefunden.'
|
default: 'Keine Daten gefunden.'
|
||||||
@ -171,7 +194,7 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['create', 'edit', 'delete', 'data-loaded'])
|
const emit = defineEmits(['create', 'edit', 'delete', 'view', 'data-loaded'])
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const data = ref([])
|
const data = ref([])
|
||||||
|
|||||||
68
assets/js/stores/permissions.js
Normal file
68
assets/js/stores/permissions.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const usePermissionStore = defineStore('permissions', () => {
|
||||||
|
const permissions = ref({})
|
||||||
|
const isAdmin = ref(false)
|
||||||
|
const loaded = ref(false)
|
||||||
|
|
||||||
|
async function loadPermissions() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/permissions', {
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
permissions.value = data.permissions
|
||||||
|
isAdmin.value = data.isAdmin
|
||||||
|
loaded.value = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load permissions:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPermission(module, action) {
|
||||||
|
if (isAdmin.value) return true
|
||||||
|
return permissions.value[module]?.[action] || false
|
||||||
|
}
|
||||||
|
|
||||||
|
function canView(module) {
|
||||||
|
return hasPermission(module, 'view')
|
||||||
|
}
|
||||||
|
|
||||||
|
function canCreate(module) {
|
||||||
|
return hasPermission(module, 'create')
|
||||||
|
}
|
||||||
|
|
||||||
|
function canEdit(module) {
|
||||||
|
return hasPermission(module, 'edit')
|
||||||
|
}
|
||||||
|
|
||||||
|
function canDelete(module) {
|
||||||
|
return hasPermission(module, 'delete')
|
||||||
|
}
|
||||||
|
|
||||||
|
function canExport(module) {
|
||||||
|
return hasPermission(module, 'export')
|
||||||
|
}
|
||||||
|
|
||||||
|
function canManage(module) {
|
||||||
|
return hasPermission(module, 'manage')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
permissions,
|
||||||
|
isAdmin,
|
||||||
|
loaded,
|
||||||
|
loadPermissions,
|
||||||
|
hasPermission,
|
||||||
|
canView,
|
||||||
|
canCreate,
|
||||||
|
canEdit,
|
||||||
|
canDelete,
|
||||||
|
canExport,
|
||||||
|
canManage
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -7,6 +7,10 @@
|
|||||||
:columns="contactColumns"
|
:columns="contactColumns"
|
||||||
data-source="/api/contacts"
|
data-source="/api/contacts"
|
||||||
storage-key="contactTableColumns"
|
storage-key="contactTableColumns"
|
||||||
|
:show-view-button="canView"
|
||||||
|
:show-edit-button="canEdit"
|
||||||
|
:show-delete-button="canDelete"
|
||||||
|
@view="viewContact"
|
||||||
@create="openNewContactDialog"
|
@create="openNewContactDialog"
|
||||||
@edit="editContact"
|
@edit="editContact"
|
||||||
@delete="confirmDelete"
|
@delete="confirmDelete"
|
||||||
@ -353,12 +357,192 @@
|
|||||||
<Button label="Löschen" severity="danger" @click="deleteContact" :loading="deleting" />
|
<Button label="Löschen" severity="danger" @click="deleteContact" :loading="deleting" />
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- View Contact Dialog (Read-Only) -->
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="viewDialog"
|
||||||
|
header="Kontakt anzeigen"
|
||||||
|
:modal="true"
|
||||||
|
:style="{ width: '800px' }"
|
||||||
|
>
|
||||||
|
<div v-if="viewingContact" class="flex flex-col gap-4">
|
||||||
|
<!-- Company Information -->
|
||||||
|
<div class="font-semibold">Firmeninformationen</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-medium text-sm text-500">Firmenname</label>
|
||||||
|
<div>{{ viewingContact.companyName }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-medium text-sm text-500">Kundennummer</label>
|
||||||
|
<div>{{ viewingContact.companyNumber || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Address -->
|
||||||
|
<Divider />
|
||||||
|
<div class="font-semibold">Adresse</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="flex flex-col gap-2 col-span-2">
|
||||||
|
<label class="font-medium text-sm text-500">Straße</label>
|
||||||
|
<div>{{ viewingContact.street || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-medium text-sm text-500">PLZ</label>
|
||||||
|
<div>{{ viewingContact.zipCode || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-medium text-sm text-500">Ort</label>
|
||||||
|
<div>{{ viewingContact.city || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 col-span-2">
|
||||||
|
<label class="font-medium text-sm text-500">Land</label>
|
||||||
|
<div>{{ viewingContact.country || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Information -->
|
||||||
|
<Divider />
|
||||||
|
<div class="font-semibold">Kontaktinformationen</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-medium text-sm text-500">Telefon</label>
|
||||||
|
<div>
|
||||||
|
<a v-if="viewingContact.phone" :href="'tel:' + viewingContact.phone" class="text-primary">
|
||||||
|
{{ viewingContact.phone }}
|
||||||
|
</a>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-medium text-sm text-500">Fax</label>
|
||||||
|
<div>{{ viewingContact.fax || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-medium text-sm text-500">E-Mail</label>
|
||||||
|
<div>
|
||||||
|
<a v-if="viewingContact.email" :href="'mailto:' + viewingContact.email" class="text-primary">
|
||||||
|
{{ viewingContact.email }}
|
||||||
|
</a>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-medium text-sm text-500">Website</label>
|
||||||
|
<div>
|
||||||
|
<a v-if="viewingContact.website" :href="viewingContact.website" target="_blank" class="text-primary">
|
||||||
|
{{ viewingContact.website }}
|
||||||
|
</a>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tax Information -->
|
||||||
|
<Divider />
|
||||||
|
<div class="font-semibold">Steuerinformationen</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-medium text-sm text-500">Steuernummer</label>
|
||||||
|
<div>{{ viewingContact.taxNumber || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-medium text-sm text-500">USt-IdNr.</label>
|
||||||
|
<div>{{ viewingContact.vatNumber || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Type and Status -->
|
||||||
|
<Divider />
|
||||||
|
<div class="font-semibold">Typ und Status</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<Tag v-if="viewingContact.isDebtor" value="Debitor" severity="success" />
|
||||||
|
<Tag v-if="viewingContact.isCreditor" value="Kreditor" severity="warning" />
|
||||||
|
<Tag :value="viewingContact.isActive ? 'Aktiv' : 'Inaktiv'"
|
||||||
|
:severity="viewingContact.isActive ? 'success' : 'secondary'" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<Divider />
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-medium text-sm text-500">Notizen</label>
|
||||||
|
<div class="whitespace-pre-wrap">{{ viewingContact.notes || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Persons -->
|
||||||
|
<Divider />
|
||||||
|
<div class="font-semibold">Ansprechpartner</div>
|
||||||
|
<div v-if="viewingContact.contactPersons?.length > 0" class="flex flex-col gap-3">
|
||||||
|
<div v-for="(person, index) in viewingContact.contactPersons" :key="index"
|
||||||
|
class="border p-3 rounded-md">
|
||||||
|
<div class="flex justify-between items-start mb-3">
|
||||||
|
<div class="font-medium">
|
||||||
|
{{ person.salutation }} {{ person.title }} {{ person.firstName }} {{ person.lastName }}
|
||||||
|
<Tag v-if="person.isPrimary" value="Primär" severity="info" class="ml-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div v-if="person.position">
|
||||||
|
<span class="text-500">Position:</span> {{ person.position }}
|
||||||
|
</div>
|
||||||
|
<div v-if="person.department">
|
||||||
|
<span class="text-500">Abteilung:</span> {{ person.department }}
|
||||||
|
</div>
|
||||||
|
<div v-if="person.phone">
|
||||||
|
<span class="text-500">Telefon:</span>
|
||||||
|
<a :href="'tel:' + person.phone" class="text-primary">{{ person.phone }}</a>
|
||||||
|
</div>
|
||||||
|
<div v-if="person.mobile">
|
||||||
|
<span class="text-500">Mobil:</span>
|
||||||
|
<a :href="'tel:' + person.mobile" class="text-primary">{{ person.mobile }}</a>
|
||||||
|
</div>
|
||||||
|
<div v-if="person.email" class="col-span-2">
|
||||||
|
<span class="text-500">E-Mail:</span>
|
||||||
|
<a :href="'mailto:' + person.email" class="text-primary">{{ person.email }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-500">Keine Ansprechpartner</div>
|
||||||
|
|
||||||
|
<!-- Timestamps -->
|
||||||
|
<Divider />
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-sm text-500">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Erstellt am:</span> {{ formatDate(viewingContact.createdAt) }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Zuletzt geändert:</span> {{ formatDate(viewingContact.updatedAt) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button label="Schließen" @click="viewDialog = false" />
|
||||||
|
<Button
|
||||||
|
v-if="canEdit"
|
||||||
|
label="Bearbeiten"
|
||||||
|
icon="pi pi-pencil"
|
||||||
|
@click="editFromView"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useToast } from 'primevue/usetoast'
|
import { useToast } from 'primevue/usetoast'
|
||||||
|
import { usePermissionStore } from '../stores/permissions'
|
||||||
import CrudDataTable from '../components/CrudDataTable.vue'
|
import CrudDataTable from '../components/CrudDataTable.vue'
|
||||||
import Dialog from 'primevue/dialog'
|
import Dialog from 'primevue/dialog'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
@ -371,6 +555,13 @@ import Divider from 'primevue/divider'
|
|||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const tableRef = ref(null)
|
const tableRef = ref(null)
|
||||||
|
const permissionStore = usePermissionStore()
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
const canView = computed(() => permissionStore.canView('contacts'))
|
||||||
|
const canCreate = computed(() => permissionStore.canCreate('contacts'))
|
||||||
|
const canEdit = computed(() => permissionStore.canEdit('contacts'))
|
||||||
|
const canDelete = computed(() => permissionStore.canDelete('contacts'))
|
||||||
|
|
||||||
// Column definitions
|
// Column definitions
|
||||||
const contactColumns = [
|
const contactColumns = [
|
||||||
@ -396,8 +587,10 @@ const contactColumns = [
|
|||||||
|
|
||||||
// State
|
// State
|
||||||
const contactDialog = ref(false)
|
const contactDialog = ref(false)
|
||||||
|
const viewDialog = ref(false)
|
||||||
const deleteDialog = ref(false)
|
const deleteDialog = ref(false)
|
||||||
const editingContact = ref(null)
|
const editingContact = ref(null)
|
||||||
|
const viewingContact = ref(null)
|
||||||
const submitted = ref(false)
|
const submitted = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const deleting = ref(false)
|
const deleting = ref(false)
|
||||||
@ -470,6 +663,11 @@ const openNewContactDialog = () => {
|
|||||||
contactDialog.value = true
|
contactDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewContact = (contact) => {
|
||||||
|
viewingContact.value = { ...contact }
|
||||||
|
viewDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const editContact = (contact) => {
|
const editContact = (contact) => {
|
||||||
editingContact.value = { ...contact }
|
editingContact.value = { ...contact }
|
||||||
submitted.value = false
|
submitted.value = false
|
||||||
@ -600,6 +798,13 @@ const setPrimaryContact = (index) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const editFromView = () => {
|
||||||
|
editingContact.value = { ...viewingContact.value }
|
||||||
|
viewDialog.value = false
|
||||||
|
submitted.value = false
|
||||||
|
contactDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const onDataLoaded = (data) => {
|
const onDataLoaded = (data) => {
|
||||||
// Optional: Do something when data is loaded
|
// Optional: Do something when data is loaded
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/Controller/PermissionController.php
Normal file
45
src/Controller/PermissionController.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[IsGranted('ROLE_USER')]
|
||||||
|
class PermissionController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/api/permissions', name: 'api_permissions', methods: ['GET'])]
|
||||||
|
public function getPermissions(): JsonResponse
|
||||||
|
{
|
||||||
|
/** @var User|null $user */
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return new JsonResponse(['error' => 'Not authenticated'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$permissions = [];
|
||||||
|
|
||||||
|
// Liste aller Module die geprüft werden sollen
|
||||||
|
$modules = ['contacts', 'users', 'roles', 'settings'];
|
||||||
|
|
||||||
|
foreach ($modules as $module) {
|
||||||
|
$permissions[$module] = [
|
||||||
|
'view' => $user->hasModulePermission($module, 'view'),
|
||||||
|
'create' => $user->hasModulePermission($module, 'create'),
|
||||||
|
'edit' => $user->hasModulePermission($module, 'edit'),
|
||||||
|
'delete' => $user->hasModulePermission($module, 'delete'),
|
||||||
|
'export' => $user->hasModulePermission($module, 'export'),
|
||||||
|
'manage' => $user->hasModulePermission($module, 'manage'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'permissions' => $permissions,
|
||||||
|
'isAdmin' => in_array('ROLE_ADMIN', $user->getRoles())
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ use ApiPlatform\Metadata\GetCollection;
|
|||||||
use ApiPlatform\Metadata\Post;
|
use ApiPlatform\Metadata\Post;
|
||||||
use ApiPlatform\Metadata\Put;
|
use ApiPlatform\Metadata\Put;
|
||||||
use ApiPlatform\Metadata\Delete;
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use App\Entity\Interface\ModuleAwareInterface;
|
||||||
use App\Repository\ContactRepository;
|
use App\Repository\ContactRepository;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
@ -23,11 +24,26 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
#[ORM\Table(name: 'contacts')]
|
#[ORM\Table(name: 'contacts')]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
operations: [
|
operations: [
|
||||||
new GetCollection(stateless: false),
|
new GetCollection(
|
||||||
new Get(stateless: false),
|
security: "is_granted('VIEW', 'contacts')",
|
||||||
new Post(security: "is_granted('ROLE_USER')", stateless: false),
|
stateless: false
|
||||||
new Put(security: "is_granted('ROLE_USER')", stateless: false),
|
),
|
||||||
new Delete(security: "is_granted('ROLE_ADMIN')", stateless: false)
|
new Get(
|
||||||
|
security: "is_granted('VIEW', object)",
|
||||||
|
stateless: false
|
||||||
|
),
|
||||||
|
new Post(
|
||||||
|
security: "is_granted('CREATE', 'contacts')",
|
||||||
|
stateless: false
|
||||||
|
),
|
||||||
|
new Put(
|
||||||
|
security: "is_granted('EDIT', object)",
|
||||||
|
stateless: false
|
||||||
|
),
|
||||||
|
new Delete(
|
||||||
|
security: "is_granted('DELETE', object)",
|
||||||
|
stateless: false
|
||||||
|
)
|
||||||
],
|
],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationItemsPerPage: 30,
|
paginationItemsPerPage: 30,
|
||||||
@ -38,7 +54,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
)]
|
)]
|
||||||
#[ApiFilter(BooleanFilter::class, properties: ['isDebtor', 'isCreditor', 'isActive'])]
|
#[ApiFilter(BooleanFilter::class, properties: ['isDebtor', 'isCreditor', 'isActive'])]
|
||||||
#[ApiFilter(SearchFilter::class, properties: ['companyName' => 'partial', 'city' => 'partial', 'email' => 'partial'])]
|
#[ApiFilter(SearchFilter::class, properties: ['companyName' => 'partial', 'city' => 'partial', 'email' => 'partial'])]
|
||||||
class Contact
|
class Contact implements ModuleAwareInterface
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
@ -394,4 +410,13 @@ class Contact
|
|||||||
{
|
{
|
||||||
return $this->companyName ?? '';
|
return $this->companyName ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the module code this entity belongs to.
|
||||||
|
* Required by ModuleVoter for permission checks.
|
||||||
|
*/
|
||||||
|
public function getModuleName(): string
|
||||||
|
{
|
||||||
|
return 'contacts';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/Entity/Interface/ModuleAwareInterface.php
Normal file
14
src/Entity/Interface/ModuleAwareInterface.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity\Interface;
|
||||||
|
|
||||||
|
interface ModuleAwareInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the module code this entity belongs to.
|
||||||
|
* Must match the 'code' field in the modules table.
|
||||||
|
*
|
||||||
|
* @return string Module code (e.g., 'contacts', 'deals', 'users')
|
||||||
|
*/
|
||||||
|
public function getModuleName(): string;
|
||||||
|
}
|
||||||
@ -60,7 +60,7 @@ class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
|
|||||||
return new RedirectResponse($targetPath);
|
return new RedirectResponse($targetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RedirectResponse($this->router->generate('app_dashboard'));
|
return new RedirectResponse($this->router->generate('app_home'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getLoginUrl(Request $request): string
|
protected function getLoginUrl(Request $request): string
|
||||||
|
|||||||
@ -2,31 +2,37 @@
|
|||||||
|
|
||||||
namespace App\Security\Voter;
|
namespace App\Security\Voter;
|
||||||
|
|
||||||
|
use App\Entity\Interface\ModuleAwareInterface;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
class ModuleVoter extends Voter
|
class ModuleVoter extends Voter
|
||||||
{
|
{
|
||||||
public const VIEW = 'MODULE_VIEW';
|
public const VIEW = 'VIEW';
|
||||||
public const CREATE = 'MODULE_CREATE';
|
public const CREATE = 'CREATE';
|
||||||
public const EDIT = 'MODULE_EDIT';
|
public const EDIT = 'EDIT';
|
||||||
public const DELETE = 'MODULE_DELETE';
|
public const DELETE = 'DELETE';
|
||||||
public const EXPORT = 'MODULE_EXPORT';
|
public const EXPORT = 'EXPORT';
|
||||||
public const MANAGE = 'MODULE_MANAGE';
|
public const MANAGE = 'MANAGE';
|
||||||
|
|
||||||
protected function supports(string $attribute, mixed $subject): bool
|
protected function supports(string $attribute, mixed $subject): bool
|
||||||
{
|
{
|
||||||
// Der Voter unterstützt MODULE_* Attribute
|
$supportedAttributes = [
|
||||||
// Subject ist der Module-Code als String (z.B. 'contacts', 'deals')
|
|
||||||
return in_array($attribute, [
|
|
||||||
self::VIEW,
|
self::VIEW,
|
||||||
self::CREATE,
|
self::CREATE,
|
||||||
self::EDIT,
|
self::EDIT,
|
||||||
self::DELETE,
|
self::DELETE,
|
||||||
self::EXPORT,
|
self::EXPORT,
|
||||||
self::MANAGE,
|
self::MANAGE,
|
||||||
]) && is_string($subject);
|
];
|
||||||
|
|
||||||
|
if (!in_array($attribute, $supportedAttributes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject can be a string (module name) or an object implementing ModuleAwareInterface
|
||||||
|
return is_string($subject) || $subject instanceof ModuleAwareInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||||
@ -48,8 +54,8 @@ class ModuleVoter extends Voter
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// $subject ist der Module-Code (z.B. 'contacts')
|
// Extract module name from subject
|
||||||
$moduleCode = $subject;
|
$moduleCode = is_string($subject) ? $subject : $subject->getModuleName();
|
||||||
|
|
||||||
// Map Voter-Attribute auf Permission-Actions
|
// Map Voter-Attribute auf Permission-Actions
|
||||||
$action = match($attribute) {
|
$action = match($attribute) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user