Compare commits

...

2 Commits

13 changed files with 958 additions and 18 deletions

View File

@ -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">

View File

@ -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({

View 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>

View File

@ -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
});
@ -229,16 +266,66 @@ const errors = ref({});
const fetchUsers = async () => {
loading.value = true;
try {
const response = await fetch('/api/users');
const response = await fetch('/api/users', {
credentials: 'same-origin',
headers: {
'Accept': 'application/ld+json'
}
});
if (!response.ok) {
const errorText = await response.text();
console.error('API Error:', response.status, errorText);
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
users.value = data['hydra:member'] || data;
// 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) {
toast.add({ severity: 'error', summary: 'Fehler', detail: 'Benutzer konnten nicht geladen werden', life: 3000 });
console.error('Error fetching users:', error);
toast.add({
severity: 'error',
summary: 'Fehler',
detail: 'Benutzer konnten nicht geladen werden: ' + error.message,
life: 5000
});
} finally {
loading.value = false;
}
};
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;
@ -248,6 +335,7 @@ const openCreateDialog = () => {
email: '',
plainPassword: '',
roles: ['ROLE_USER'],
userRoles: [],
isActive: true
};
errors.value = {};
@ -260,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;
@ -297,7 +386,11 @@ const saveUser = async () => {
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
headers: {
'Content-Type': 'application/ld+json',
'Accept': 'application/ld+json'
},
body: JSON.stringify(payload)
});
@ -328,7 +421,8 @@ const deleteUser = async () => {
deleting.value = true;
try {
const response = await fetch(`/api/users/${userToDelete.value.id}`, {
method: 'DELETE'
method: 'DELETE',
credentials: 'same-origin'
});
if (!response.ok) throw new Error('Fehler beim Löschen');
@ -356,6 +450,8 @@ const formatDate = (dateString) => {
onMounted(() => {
fetchUsers();
fetchRoles();
fetchModules();
});
</script>
@ -547,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>

View File

@ -139,6 +139,7 @@ body {
}
&.p-tag-info {
background: #3b82f6;
background: #93c5fd;
color: #1e3a8a;
}
}

View File

@ -1,7 +1,11 @@
api_platform:
title: Hello API Platform
title: myCRM API
version: 1.0.0
defaults:
stateless: true
stateless: false
cache_headers:
vary: ['Content-Type', 'Authorization', 'Origin']
formats:
jsonld: ['application/ld+json']
json: ['application/json']
html: ['text/html']

View File

@ -1,5 +1,6 @@
framework:
asset_mapper:
enabled: false
# The paths to make available to the asset mapper.
paths:
- assets/
@ -8,4 +9,5 @@ framework:
when@prod:
framework:
asset_mapper:
enabled: false
missing_import_mode: warn

6
cookies.txt Normal file
View 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

View File

@ -8,7 +8,7 @@ use Symfony\Component\Routing\Attribute\Route;
class HomeController extends AbstractController
{
#[Route('/{reactRouting}', name: 'app_home', requirements: ['reactRouting' => '(?!login|logout|api).*'], defaults: ['reactRouting' => null], priority: -1)]
#[Route('/{reactRouting}', name: 'app_home', requirements: ['reactRouting' => '(?!login|logout|api|bundles).*'], defaults: ['reactRouting' => null], priority: -1)]
public function index(): Response
{
return $this->render('base.html.twig');

View File

@ -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;

View File

@ -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]

View File

@ -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;

View File

@ -21,11 +21,11 @@ use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Put(security: "is_granted('ROLE_ADMIN') or object == user"),
new Delete(security: "is_granted('ROLE_ADMIN')")
new GetCollection(stateless: false),
new Get(stateless: false),
new Post(security: "is_granted('ROLE_ADMIN')", stateless: false),
new Put(security: "is_granted('ROLE_ADMIN') or object == user", stateless: false),
new Delete(security: "is_granted('ROLE_ADMIN')", stateless: false)
],
normalizationContext: ['groups' => ['user:read']],
denormalizationContext: ['groups' => ['user:write']]
@ -204,10 +204,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return trim($this->firstName . ' ' . $this->lastName);
}
#[Groups(['user:read', 'user:write'])]
public function isActive(): bool
{
return $this->isActive;
}
// Alias for Symfony Serializer (which expects get* prefix)
public function getIsActive(): bool
{
return $this->isActive;
}
public function setIsActive(bool $isActive): static
{