feat: Add role management functionality with CRUD operations and update user roles handling
This commit is contained in:
parent
465515191f
commit
07c675968a
@ -30,6 +30,9 @@
|
||||
<RouterLink to="/users" v-if="authStore.isAdmin" @click="closeMobileMenu">
|
||||
<i class="pi pi-user-edit"></i> Benutzerverwaltung
|
||||
</RouterLink>
|
||||
<RouterLink to="/roles" v-if="authStore.isAdmin" @click="closeMobileMenu">
|
||||
<i class="pi pi-shield"></i> Rollenverwaltung
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
<div class="user-info" v-if="authStore.isAuthenticated">
|
||||
|
||||
@ -4,6 +4,7 @@ import ContactList from './views/ContactList.vue';
|
||||
import CompanyList from './views/CompanyList.vue';
|
||||
import DealList from './views/DealList.vue';
|
||||
import UserManagement from './views/UserManagement.vue';
|
||||
import RoleManagement from './views/RoleManagement.vue';
|
||||
|
||||
const routes = [
|
||||
{ path: '/', name: 'dashboard', component: Dashboard },
|
||||
@ -11,6 +12,7 @@ const routes = [
|
||||
{ path: '/companies', name: 'companies', component: CompanyList },
|
||||
{ path: '/deals', name: 'deals', component: DealList },
|
||||
{ path: '/users', name: 'users', component: UserManagement, meta: { requiresAdmin: true } },
|
||||
{ path: '/roles', name: 'roles', component: RoleManagement, meta: { requiresAdmin: true } },
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
688
assets/js/views/RoleManagement.vue
Normal file
688
assets/js/views/RoleManagement.vue
Normal file
@ -0,0 +1,688 @@
|
||||
<template>
|
||||
<div class="role-management">
|
||||
<div class="page-header">
|
||||
<h2>Rollenverwaltung</h2>
|
||||
<Button
|
||||
label="Neue Rolle"
|
||||
icon="pi pi-plus"
|
||||
@click="openCreateDialog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
:value="roles"
|
||||
:loading="loading"
|
||||
stripedRows
|
||||
showGridlines
|
||||
>
|
||||
<Column field="name" header="Name" sortable style="width: 200px" />
|
||||
<Column field="description" header="Beschreibung" sortable />
|
||||
<Column field="isSystem" header="System-Rolle" sortable style="width: 150px">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="data.isSystem ? 'Ja' : 'Nein'" :severity="data.isSystem ? 'warning' : 'secondary'" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="Aktionen" style="width: 150px">
|
||||
<template #body="{ data }">
|
||||
<div class="action-buttons">
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
severity="info"
|
||||
text
|
||||
rounded
|
||||
@click="openEditDialog(data)"
|
||||
:disabled="data.isSystem"
|
||||
v-tooltip.top="data.isSystem ? 'System-Rollen können nicht bearbeitet werden' : 'Bearbeiten'"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
text
|
||||
rounded
|
||||
@click="confirmDelete(data)"
|
||||
:disabled="data.isSystem"
|
||||
v-tooltip.top="data.isSystem ? 'System-Rollen können nicht gelöscht werden' : 'Löschen'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
|
||||
<!-- Create/Edit Dialog -->
|
||||
<Dialog
|
||||
v-model:visible="dialogVisible"
|
||||
:header="isEditMode ? 'Rolle bearbeiten' : 'Neue Rolle erstellen'"
|
||||
modal
|
||||
:style="{ width: '90vw', maxWidth: '900px' }"
|
||||
class="role-dialog"
|
||||
>
|
||||
<div class="form-container">
|
||||
<div class="form-field">
|
||||
<label for="name">Name *</label>
|
||||
<InputText
|
||||
id="name"
|
||||
v-model="formData.name"
|
||||
:class="{ 'p-invalid': errors.name }"
|
||||
placeholder="z.B. Vertriebsmitarbeiter"
|
||||
/>
|
||||
<small v-if="errors.name" class="p-error">{{ errors.name }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="description">Beschreibung</label>
|
||||
<Textarea
|
||||
id="description"
|
||||
v-model="formData.description"
|
||||
rows="3"
|
||||
placeholder="Beschreibung der Rolle und ihrer Aufgaben"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="permissions-section">
|
||||
<h3>Modulberechtigungen</h3>
|
||||
<div class="permissions-matrix">
|
||||
<div class="matrix-header">
|
||||
<div class="module-cell">Modul</div>
|
||||
<div class="permission-cell" v-tooltip.top="'Benutzer kann Daten ansehen'">
|
||||
<i class="pi pi-eye"></i>
|
||||
<span>Lesen</span>
|
||||
</div>
|
||||
<div class="permission-cell" v-tooltip.top="'Benutzer kann neue Einträge erstellen'">
|
||||
<i class="pi pi-plus"></i>
|
||||
<span>Erstellen</span>
|
||||
</div>
|
||||
<div class="permission-cell" v-tooltip.top="'Benutzer kann bestehende Einträge bearbeiten'">
|
||||
<i class="pi pi-pencil"></i>
|
||||
<span>Bearbeiten</span>
|
||||
</div>
|
||||
<div class="permission-cell" v-tooltip.top="'Benutzer kann Einträge löschen'">
|
||||
<i class="pi pi-trash"></i>
|
||||
<span>Löschen</span>
|
||||
</div>
|
||||
<div class="permission-cell" v-tooltip.top="'Benutzer kann Daten exportieren'">
|
||||
<i class="pi pi-download"></i>
|
||||
<span>Export</span>
|
||||
</div>
|
||||
<div class="permission-cell" v-tooltip.top="'Benutzer kann Modul-Einstellungen verwalten'">
|
||||
<i class="pi pi-cog"></i>
|
||||
<span>Verwalten</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="module in modules"
|
||||
:key="module.id"
|
||||
class="matrix-row"
|
||||
>
|
||||
<div class="module-cell">
|
||||
<i :class="module.icon"></i>
|
||||
<span>{{ module.name }}</span>
|
||||
</div>
|
||||
<div class="permission-cell">
|
||||
<Checkbox
|
||||
v-model="getPermission(module.id).canView"
|
||||
:binary="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="permission-cell">
|
||||
<Checkbox
|
||||
v-model="getPermission(module.id).canCreate"
|
||||
:binary="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="permission-cell">
|
||||
<Checkbox
|
||||
v-model="getPermission(module.id).canEdit"
|
||||
:binary="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="permission-cell">
|
||||
<Checkbox
|
||||
v-model="getPermission(module.id).canDelete"
|
||||
:binary="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="permission-cell">
|
||||
<Checkbox
|
||||
v-model="getPermission(module.id).canExport"
|
||||
:binary="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="permission-cell">
|
||||
<Checkbox
|
||||
v-model="getPermission(module.id).canManage"
|
||||
:binary="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="Abbrechen" severity="secondary" @click="dialogVisible = false" />
|
||||
<Button label="Speichern" @click="saveRole" :loading="saving" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Delete Confirmation -->
|
||||
<Dialog
|
||||
v-model:visible="deleteDialogVisible"
|
||||
header="Rolle löschen"
|
||||
modal
|
||||
:style="{ width: '450px' }"
|
||||
>
|
||||
<p>Möchten Sie die Rolle <strong>{{ roleToDelete?.name }}</strong> wirklich löschen?</p>
|
||||
<p class="text-muted">Diese Aktion kann nicht rückgängig gemacht werden.</p>
|
||||
|
||||
<template #footer>
|
||||
<Button label="Abbrechen" severity="secondary" @click="deleteDialogVisible = false" />
|
||||
<Button label="Löschen" severity="danger" @click="deleteRole" :loading="deleting" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Column from 'primevue/column';
|
||||
import Button from 'primevue/button';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Textarea from 'primevue/textarea';
|
||||
import Checkbox from 'primevue/checkbox';
|
||||
import Tag from 'primevue/tag';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const toast = useToast();
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const deleting = ref(false);
|
||||
const roles = ref([]);
|
||||
const modules = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const deleteDialogVisible = ref(false);
|
||||
const isEditMode = ref(false);
|
||||
const roleToDelete = ref(null);
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
isSystem: false,
|
||||
permissions: []
|
||||
});
|
||||
|
||||
const errors = ref({});
|
||||
|
||||
const fetchRoles = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await fetch('/api/roles', {
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Accept': 'application/ld+json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
roles.value = data.member || [];
|
||||
console.log('Loaded roles:', roles.value);
|
||||
} catch (error) {
|
||||
console.error('Error fetching roles:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Fehler',
|
||||
detail: 'Rollen konnten nicht geladen werden',
|
||||
life: 5000
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchModules = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/modules', {
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Accept': 'application/ld+json'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Modules API response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Modules API error:', response.status, errorText);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Modules API raw data:', data);
|
||||
console.log('First module:', data.member[0]);
|
||||
console.log('First module isActive:', data.member[0]?.isActive);
|
||||
|
||||
const allModules = data.member || data['hydra:member'] || [];
|
||||
console.log('All modules before filter:', allModules);
|
||||
|
||||
modules.value = allModules
|
||||
.filter(m => {
|
||||
console.log(`Module ${m.name}: isActive = ${m.isActive}`);
|
||||
return m.isActive;
|
||||
})
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
|
||||
console.log('Loaded modules:', modules.value);
|
||||
} catch (error) {
|
||||
console.error('Error fetching modules:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Fehler',
|
||||
detail: 'Module konnten nicht geladen werden: ' + error.message,
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getPermission = (moduleId) => {
|
||||
let permission = formData.value.permissions.find(p => p.moduleId === moduleId);
|
||||
if (!permission) {
|
||||
permission = {
|
||||
moduleId,
|
||||
canView: false,
|
||||
canCreate: false,
|
||||
canEdit: false,
|
||||
canDelete: false,
|
||||
canExport: false,
|
||||
canManage: false
|
||||
};
|
||||
formData.value.permissions.push(permission);
|
||||
}
|
||||
console.log(`getPermission(${moduleId}):`, permission);
|
||||
return permission;
|
||||
};
|
||||
|
||||
const openCreateDialog = () => {
|
||||
isEditMode.value = false;
|
||||
formData.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
isSystem: false,
|
||||
permissions: []
|
||||
};
|
||||
errors.value = {};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const openEditDialog = async (role) => {
|
||||
isEditMode.value = true;
|
||||
|
||||
// Load full role details with permissions from API
|
||||
try {
|
||||
const response = await fetch(role['@id'] || `/api/roles/${role.id}`, {
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Accept': 'application/ld+json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to load role details');
|
||||
|
||||
const fullRole = await response.json();
|
||||
console.log('Loaded full role for editing:', fullRole);
|
||||
|
||||
formData.value = {
|
||||
id: fullRole.id,
|
||||
'@id': fullRole['@id'],
|
||||
name: fullRole.name,
|
||||
description: fullRole.description || '',
|
||||
isSystem: fullRole.isSystem,
|
||||
permissions: []
|
||||
};
|
||||
|
||||
// Convert role.permissions to our internal format
|
||||
if (fullRole.permissions && fullRole.permissions.length > 0) {
|
||||
fullRole.permissions.forEach(perm => {
|
||||
console.log('Processing permission:', perm);
|
||||
formData.value.permissions.push({
|
||||
id: perm.id,
|
||||
'@id': perm['@id'],
|
||||
moduleId: perm.module?.id || perm.module,
|
||||
canView: perm.canView ?? false,
|
||||
canCreate: perm.canCreate ?? false,
|
||||
canEdit: perm.canEdit ?? false,
|
||||
canDelete: perm.canDelete ?? false,
|
||||
canExport: perm.canExport ?? false,
|
||||
canManage: perm.canManage ?? false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Form data permissions:', formData.value.permissions);
|
||||
|
||||
errors.value = {};
|
||||
dialogVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('Error loading role:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Fehler',
|
||||
detail: 'Rolle konnte nicht geladen werden',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
errors.value = {};
|
||||
if (!formData.value.name) errors.value.name = 'Name ist erforderlich';
|
||||
return Object.keys(errors.value).length === 0;
|
||||
};
|
||||
|
||||
const saveRole = async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
// Step 1: Save role (without permissions)
|
||||
const rolePayload = {
|
||||
name: formData.value.name,
|
||||
description: formData.value.description,
|
||||
isSystem: formData.value.isSystem
|
||||
};
|
||||
|
||||
console.log('Saving role with payload:', rolePayload);
|
||||
|
||||
const url = isEditMode.value ? `/api/roles/${formData.value.id}` : '/api/roles';
|
||||
const method = isEditMode.value ? 'PUT' : 'POST';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/ld+json',
|
||||
'Accept': 'application/ld+json'
|
||||
},
|
||||
body: JSON.stringify(rolePayload)
|
||||
});
|
||||
|
||||
console.log('Save response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('API Error Response:', errorText);
|
||||
throw new Error(`Fehler beim Speichern der Rolle: ${response.status}`);
|
||||
}
|
||||
|
||||
const savedRole = await response.json();
|
||||
console.log('Saved role:', savedRole);
|
||||
|
||||
// Step 2: Delete existing permissions for this role (if editing)
|
||||
if (isEditMode.value && savedRole.permissions && savedRole.permissions.length > 0) {
|
||||
for (const perm of savedRole.permissions) {
|
||||
try {
|
||||
await fetch(perm['@id'], {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Could not delete permission:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Create new permissions
|
||||
const permissionsToCreate = formData.value.permissions
|
||||
.filter(p => p.canView || p.canCreate || p.canEdit || p.canDelete || p.canExport || p.canManage);
|
||||
|
||||
console.log('Creating permissions:', permissionsToCreate);
|
||||
|
||||
for (const perm of permissionsToCreate) {
|
||||
const permPayload = {
|
||||
role: savedRole['@id'],
|
||||
module: `/api/modules/${perm.moduleId}`,
|
||||
canView: perm.canView || false,
|
||||
canCreate: perm.canCreate || false,
|
||||
canEdit: perm.canEdit || false,
|
||||
canDelete: perm.canDelete || false,
|
||||
canExport: perm.canExport || false,
|
||||
canManage: perm.canManage || false
|
||||
};
|
||||
|
||||
const permResponse = await fetch('/api/role_permissions', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/ld+json',
|
||||
'Accept': 'application/ld+json'
|
||||
},
|
||||
body: JSON.stringify(permPayload)
|
||||
});
|
||||
|
||||
if (!permResponse.ok) {
|
||||
const errorText = await permResponse.text();
|
||||
console.error('Permission creation error:', errorText);
|
||||
}
|
||||
}
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Erfolg',
|
||||
detail: `Rolle wurde ${isEditMode.value ? 'aktualisiert' : 'erstellt'}`,
|
||||
life: 3000
|
||||
});
|
||||
|
||||
dialogVisible.value = false;
|
||||
await fetchRoles();
|
||||
} catch (error) {
|
||||
console.error('Error saving role:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Fehler',
|
||||
detail: 'Rolle konnte nicht gespeichert werden: ' + error.message,
|
||||
life: 3000
|
||||
});
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = (role) => {
|
||||
roleToDelete.value = role;
|
||||
deleteDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const deleteRole = async () => {
|
||||
deleting.value = true;
|
||||
try {
|
||||
const response = await fetch(`/api/roles/${roleToDelete.value.id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Fehler beim Löschen');
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Erfolg',
|
||||
detail: 'Rolle wurde gelöscht',
|
||||
life: 3000
|
||||
});
|
||||
|
||||
deleteDialogVisible.value = false;
|
||||
await fetchRoles();
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Fehler',
|
||||
detail: 'Rolle konnte nicht gelöscht werden',
|
||||
life: 3000
|
||||
});
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchRoles();
|
||||
fetchModules();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.role-management {
|
||||
padding: 1.5rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.role-dialog {
|
||||
.form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.p-error {
|
||||
color: var(--p-red-500);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.permissions-section {
|
||||
h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.permissions-matrix {
|
||||
border: 1px solid var(--p-surface-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
.matrix-header,
|
||||
.matrix-row {
|
||||
display: grid;
|
||||
grid-template-columns: 200px repeat(6, 1fr);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
grid-template-columns: 150px repeat(6, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 120px repeat(6, 60px);
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.matrix-header {
|
||||
background: var(--p-surface-100);
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid var(--p-surface-border);
|
||||
|
||||
.permission-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
|
||||
i {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.matrix-row {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--p-surface-border);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--p-surface-50);
|
||||
}
|
||||
}
|
||||
|
||||
.module-cell,
|
||||
.permission-cell {
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.module-cell {
|
||||
justify-content: flex-start;
|
||||
font-weight: 500;
|
||||
border-right: 1px solid var(--p-surface-border);
|
||||
|
||||
i {
|
||||
color: var(--p-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.permission-cell {
|
||||
border-right: 1px solid var(--p-surface-border);
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--p-text-muted-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
@ -30,9 +30,18 @@
|
||||
<Tag :value="data.isActive ? 'Aktiv' : 'Inaktiv'" :severity="data.isActive ? 'success' : 'danger'" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="roles" header="Rollen" style="width: 150px">
|
||||
<Column field="userRoles" header="Rollen" style="width: 200px">
|
||||
<template #body="{ data }">
|
||||
<Tag v-for="role in data.roles" :key="role" :value="role" severity="info" class="mr-1" />
|
||||
<Tag
|
||||
v-for="role in data.userRoles"
|
||||
:key="role['@id'] || role.id"
|
||||
:value="role.name"
|
||||
severity="info"
|
||||
class="mr-1"
|
||||
/>
|
||||
<span v-if="!data.userRoles || data.userRoles.length === 0" class="text-muted">
|
||||
Keine Rollen
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="lastLoginAt" header="Letzter Login" sortable style="width: 180px">
|
||||
@ -130,7 +139,30 @@
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label>Symfony Rollen</label>
|
||||
<label for="userRoles">Rollen</label>
|
||||
<MultiSelect
|
||||
id="userRoles"
|
||||
v-model="formData.userRoles"
|
||||
:options="roles"
|
||||
optionLabel="name"
|
||||
optionValue="@id"
|
||||
placeholder="Rollen auswählen"
|
||||
display="chip"
|
||||
filter
|
||||
:loading="roles.length === 0"
|
||||
class="w-full"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="role-option">
|
||||
<div class="role-name">{{ slotProps.option.name }}</div>
|
||||
<div class="role-description">{{ slotProps.option.description }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</MultiSelect>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label>Symfony Sicherheitsrollen</label>
|
||||
<div class="checkbox-group">
|
||||
<div class="checkbox-item">
|
||||
<Checkbox
|
||||
@ -149,6 +181,7 @@
|
||||
<label for="role_admin">ROLE_ADMIN</label>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">Diese Rollen sind für die Symfony-Sicherheit erforderlich.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
@ -199,6 +232,7 @@ import Dialog from 'primevue/dialog';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Password from 'primevue/password';
|
||||
import Checkbox from 'primevue/checkbox';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
import Tag from 'primevue/tag';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
|
||||
@ -206,6 +240,8 @@ const authStore = useAuthStore();
|
||||
const toast = useToast();
|
||||
|
||||
const users = ref([]);
|
||||
const roles = ref([]);
|
||||
const modules = ref([]);
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const deleteDialogVisible = ref(false);
|
||||
@ -221,6 +257,7 @@ const formData = ref({
|
||||
email: '',
|
||||
plainPassword: '',
|
||||
roles: ['ROLE_USER'],
|
||||
userRoles: [],
|
||||
isActive: true
|
||||
});
|
||||
|
||||
@ -246,6 +283,8 @@ const fetchUsers = async () => {
|
||||
|
||||
// API Platform JSON-LD uses 'member' (hydra:member becomes 'member' in JS)
|
||||
users.value = data.member || data['hydra:member'] || [];
|
||||
console.log('Loaded users:', users.value);
|
||||
console.log('First user userRoles:', users.value[0]?.userRoles);
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
toast.add({
|
||||
@ -259,6 +298,34 @@ const fetchUsers = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRoles = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/roles', {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Accept': 'application/ld+json' }
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to fetch roles');
|
||||
const data = await response.json();
|
||||
roles.value = data.member || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching roles:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchModules = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/modules', {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Accept': 'application/ld+json' }
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to fetch modules');
|
||||
const data = await response.json();
|
||||
modules.value = (data.member || []).filter(m => m.isActive).sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
} catch (error) {
|
||||
console.error('Error fetching modules:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const openCreateDialog = () => {
|
||||
isEditMode.value = false;
|
||||
showPasswordField.value = false;
|
||||
@ -268,6 +335,7 @@ const openCreateDialog = () => {
|
||||
email: '',
|
||||
plainPassword: '',
|
||||
roles: ['ROLE_USER'],
|
||||
userRoles: [],
|
||||
isActive: true
|
||||
};
|
||||
errors.value = {};
|
||||
@ -280,7 +348,8 @@ const openEditDialog = (user) => {
|
||||
formData.value = {
|
||||
...user,
|
||||
plainPassword: '',
|
||||
roles: user.roles || ['ROLE_USER']
|
||||
roles: user.roles || ['ROLE_USER'],
|
||||
userRoles: (user.userRoles || []).map(role => role['@id'] || role)
|
||||
};
|
||||
errors.value = {};
|
||||
dialogVisible.value = true;
|
||||
@ -381,6 +450,8 @@ const formatDate = (dateString) => {
|
||||
|
||||
onMounted(() => {
|
||||
fetchUsers();
|
||||
fetchRoles();
|
||||
fetchModules();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -572,5 +643,24 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.role-option {
|
||||
.role-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.role-description {
|
||||
font-size: 0.875rem;
|
||||
color: var(--p-text-muted-color);
|
||||
}
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--p-text-muted-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -139,6 +139,7 @@ body {
|
||||
}
|
||||
|
||||
&.p-tag-info {
|
||||
background: #3b82f6;
|
||||
background: #93c5fd;
|
||||
color: #1e3a8a;
|
||||
}
|
||||
}
|
||||
|
||||
6
cookies.txt
Normal file
6
cookies.txt
Normal file
@ -0,0 +1,6 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
#HttpOnly_localhost FALSE / FALSE 0 PHPSESSID 4c7fe7l16qpk68ne52b1ibkej5
|
||||
#HttpOnly_localhost FALSE / FALSE 0 sf_redirect %7B%22token%22%3A%220c6b33%22%2C%22route%22%3A%22_api_%5C%2Fmodules%7B._format%7D_get_collection%22%2C%22method%22%3A%22GET%22%2C%22controller%22%3A%22api_platform.symfony.main_controller%22%2C%22status_code%22%3A302%2C%22status_text%22%3A%22Found%22%7D
|
||||
@ -2,36 +2,55 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use App\Repository\ModuleRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ModuleRepository::class)]
|
||||
#[ORM\Table(name: 'modules')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(stateless: false),
|
||||
new Get(stateless: false)
|
||||
],
|
||||
normalizationContext: ['groups' => ['module:read']],
|
||||
security: "is_granted('ROLE_USER')"
|
||||
)]
|
||||
class Module
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['module:read', 'role_permission:read', 'role:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 100, unique: true)]
|
||||
#[Groups(['module:read', 'role_permission:read', 'role:read'])]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(length: 100, unique: true)]
|
||||
#[Groups(['module:read', 'role_permission:read', 'role:read'])]
|
||||
private ?string $code = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['module:read', 'role_permission:read', 'role:read'])]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['module:read', 'role_permission:read', 'role:read'])]
|
||||
private bool $isActive = true;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['module:read', 'role_permission:read', 'role:read'])]
|
||||
private int $sortOrder = 0;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
#[Groups(['module:read', 'role_permission:read', 'role:read'])]
|
||||
private ?string $icon = null;
|
||||
|
||||
/**
|
||||
@ -88,6 +107,12 @@ class Module
|
||||
return $this->isActive;
|
||||
}
|
||||
|
||||
#[Groups(['module:read', 'role_permission:read', 'role:read'])]
|
||||
public function getIsActive(): bool
|
||||
{
|
||||
return $this->isActive;
|
||||
}
|
||||
|
||||
public function setIsActive(bool $isActive): static
|
||||
{
|
||||
$this->isActive = $isActive;
|
||||
|
||||
@ -2,27 +2,50 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use App\Repository\RoleRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: RoleRepository::class)]
|
||||
#[ORM\Table(name: 'roles')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(stateless: false),
|
||||
new Get(stateless: false),
|
||||
new Post(stateless: false, security: "is_granted('ROLE_ADMIN')"),
|
||||
new Put(stateless: false, security: "is_granted('ROLE_ADMIN')"),
|
||||
new Delete(stateless: false, security: "is_granted('ROLE_ADMIN')")
|
||||
],
|
||||
normalizationContext: ['groups' => ['role:read']],
|
||||
denormalizationContext: ['groups' => ['role:write']],
|
||||
security: "is_granted('ROLE_USER')"
|
||||
)]
|
||||
class Role
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['role:read', 'user:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 100, unique: true)]
|
||||
#[Groups(['role:read', 'role:write', 'user:read'])]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['role:read', 'role:write', 'user:read'])]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['role:read', 'role:write', 'user:read'])]
|
||||
private bool $isSystem = false;
|
||||
|
||||
/**
|
||||
@ -35,6 +58,7 @@ class Role
|
||||
* @var Collection<int, RolePermission>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: RolePermission::class, mappedBy: 'role', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[Groups(['role:read', 'role:write'])]
|
||||
private Collection $permissions;
|
||||
|
||||
#[ORM\Column]
|
||||
|
||||
@ -2,43 +2,70 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use App\Repository\RolePermissionRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: RolePermissionRepository::class)]
|
||||
#[ORM\Table(name: 'role_permissions')]
|
||||
#[ORM\UniqueConstraint(name: 'role_module_unique', columns: ['role_id', 'module_id'])]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(stateless: false, security: "is_granted('ROLE_ADMIN')"),
|
||||
new Get(stateless: false, security: "is_granted('ROLE_ADMIN')"),
|
||||
new Post(stateless: false, security: "is_granted('ROLE_ADMIN')"),
|
||||
new Put(stateless: false, security: "is_granted('ROLE_ADMIN')"),
|
||||
new Delete(stateless: false, security: "is_granted('ROLE_ADMIN')")
|
||||
],
|
||||
normalizationContext: ['groups' => ['role_permission:read']],
|
||||
denormalizationContext: ['groups' => ['role_permission:write']]
|
||||
)]
|
||||
class RolePermission
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['role_permission:read', 'role:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Role::class, inversedBy: 'permissions')]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Groups(['role_permission:read', 'role_permission:write'])]
|
||||
private ?Role $role = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Module::class, inversedBy: 'permissions')]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Groups(['role_permission:read', 'role_permission:write', 'role:read'])]
|
||||
private ?Module $module = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['role_permission:read', 'role_permission:write', 'role:read'])]
|
||||
private bool $canView = false;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['role_permission:read', 'role_permission:write', 'role:read'])]
|
||||
private bool $canCreate = false;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['role_permission:read', 'role_permission:write', 'role:read'])]
|
||||
private bool $canEdit = false;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['role_permission:read', 'role_permission:write', 'role:read'])]
|
||||
private bool $canDelete = false;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['role_permission:read', 'role_permission:write', 'role:read'])]
|
||||
private bool $canExport = false;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['role_permission:read', 'role_permission:write', 'role:read'])]
|
||||
private bool $canManage = false;
|
||||
|
||||
public function getId(): ?int
|
||||
@ -73,6 +100,12 @@ class RolePermission
|
||||
return $this->canView;
|
||||
}
|
||||
|
||||
#[Groups(['role_permission:read', 'role:read'])]
|
||||
public function getCanView(): bool
|
||||
{
|
||||
return $this->canView;
|
||||
}
|
||||
|
||||
public function setCanView(bool $canView): static
|
||||
{
|
||||
$this->canView = $canView;
|
||||
@ -84,6 +117,12 @@ class RolePermission
|
||||
return $this->canCreate;
|
||||
}
|
||||
|
||||
#[Groups(['role_permission:read', 'role:read'])]
|
||||
public function getCanCreate(): bool
|
||||
{
|
||||
return $this->canCreate;
|
||||
}
|
||||
|
||||
public function setCanCreate(bool $canCreate): static
|
||||
{
|
||||
$this->canCreate = $canCreate;
|
||||
@ -95,6 +134,12 @@ class RolePermission
|
||||
return $this->canEdit;
|
||||
}
|
||||
|
||||
#[Groups(['role_permission:read', 'role:read'])]
|
||||
public function getCanEdit(): bool
|
||||
{
|
||||
return $this->canEdit;
|
||||
}
|
||||
|
||||
public function setCanEdit(bool $canEdit): static
|
||||
{
|
||||
$this->canEdit = $canEdit;
|
||||
@ -106,6 +151,12 @@ class RolePermission
|
||||
return $this->canDelete;
|
||||
}
|
||||
|
||||
#[Groups(['role_permission:read', 'role:read'])]
|
||||
public function getCanDelete(): bool
|
||||
{
|
||||
return $this->canDelete;
|
||||
}
|
||||
|
||||
public function setCanDelete(bool $canDelete): static
|
||||
{
|
||||
$this->canDelete = $canDelete;
|
||||
@ -117,6 +168,12 @@ class RolePermission
|
||||
return $this->canExport;
|
||||
}
|
||||
|
||||
#[Groups(['role_permission:read', 'role:read'])]
|
||||
public function getCanExport(): bool
|
||||
{
|
||||
return $this->canExport;
|
||||
}
|
||||
|
||||
public function setCanExport(bool $canExport): static
|
||||
{
|
||||
$this->canExport = $canExport;
|
||||
@ -128,6 +185,12 @@ class RolePermission
|
||||
return $this->canManage;
|
||||
}
|
||||
|
||||
#[Groups(['role_permission:read', 'role:read'])]
|
||||
public function getCanManage(): bool
|
||||
{
|
||||
return $this->canManage;
|
||||
}
|
||||
|
||||
public function setCanManage(bool $canManage): static
|
||||
{
|
||||
$this->canManage = $canManage;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user