From 48bbc8015b1126e354704d432e0bc6a74cf37ff2 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 17 Oct 2025 11:55:43 +0200 Subject: [PATCH] Enhance permission management by adding caching control and improving permission parsing Implement middleware to disable caching for permission routes, ensuring up-to-date responses. Update permission parsing logic in the backend to handle JSON strings more robustly, preventing errors during permission retrieval. Enhance the frontend PermissionsView with improved UI elements for managing permissions, including reset functionality and better state representation for actions. Ensure that only explicitly set permissions are saved, optimizing data handling. --- backend/routes/permissionRoutes.js | 11 + backend/services/permissionService.js | 49 ++++- frontend/src/views/PermissionsView.vue | 278 +++++++++++++++++++++++-- 3 files changed, 310 insertions(+), 28 deletions(-) diff --git a/backend/routes/permissionRoutes.js b/backend/routes/permissionRoutes.js index b7e2bf0..c77c40c 100644 --- a/backend/routes/permissionRoutes.js +++ b/backend/routes/permissionRoutes.js @@ -5,6 +5,17 @@ import permissionController from '../controllers/permissionController.js'; const router = express.Router(); +// Middleware to disable caching for permission routes +const noCache = (req, res, next) => { + res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private'); + res.set('Pragma', 'no-cache'); + res.set('Expires', '0'); + next(); +}; + +// Apply no-cache to all routes +router.use(noCache); + // Get available roles (no club context needed) router.get('/roles/available', authenticate, permissionController.getAvailableRoles); diff --git a/backend/services/permissionService.js b/backend/services/permissionService.js index 3c00aa5..02855a0 100644 --- a/backend/services/permissionService.js +++ b/backend/services/permissionService.js @@ -26,7 +26,7 @@ const ROLE_PERMISSIONS = { diary: { read: true, write: true, delete: true }, members: { read: true, write: true, delete: false }, teams: { read: true, write: true, delete: false }, - schedule: { read: true, write: true, delete: false }, + schedule: { read: true, write: false, delete: false }, tournaments: { read: true, write: true, delete: false }, statistics: { read: true, write: false }, settings: { read: false, write: false }, @@ -266,15 +266,30 @@ class PermissionService { }] }); - return userClubs.map(uc => ({ - userId: uc.userId, - user: uc.user, - role: uc.role, - isOwner: uc.isOwner, - approved: uc.approved, - permissions: uc.permissions, - effectivePermissions: this.getEffectivePermissions(uc) - })); + return userClubs.map(uc => { + // Parse permissions JSON string to object + let parsedPermissions = null; + if (uc.permissions) { + try { + parsedPermissions = typeof uc.permissions === 'string' + ? JSON.parse(uc.permissions) + : uc.permissions; + } catch (err) { + console.error('Error parsing permissions JSON:', err); + parsedPermissions = null; + } + } + + return { + userId: uc.userId, + user: uc.user, + role: uc.role, + isOwner: uc.isOwner, + approved: uc.approved, + permissions: parsedPermissions, + effectivePermissions: this.getEffectivePermissions(uc) + }; + }); } /** @@ -286,7 +301,19 @@ class PermissionService { } const rolePermissions = ROLE_PERMISSIONS[userClub.role] || ROLE_PERMISSIONS.member; - const customPermissions = userClub.permissions || {}; + + // Parse permissions JSON string to object + let customPermissions = {}; + if (userClub.permissions) { + try { + customPermissions = typeof userClub.permissions === 'string' + ? JSON.parse(userClub.permissions) + : userClub.permissions; + } catch (err) { + console.error('Error parsing permissions JSON in getEffectivePermissions:', err); + customPermissions = {}; + } + } return this.mergePermissions(rolePermissions, customPermissions); } diff --git a/frontend/src/views/PermissionsView.vue b/frontend/src/views/PermissionsView.vue index 19bd988..d38a246 100644 --- a/frontend/src/views/PermissionsView.vue +++ b/frontend/src/views/PermissionsView.vue @@ -102,21 +102,27 @@
-

{{ resource.label }}

+
+

{{ resource.label }}

+ +
- +
+ {{ getActionLabel(action) }} + +
@@ -150,7 +156,7 @@ export default { return !can('permissions', 'write'); }); - const loadData = async () => { + const loadData = async (force = false) => { loading.value = true; error.value = null; @@ -164,7 +170,8 @@ export default { permissionStructure.value = structureResponse.data; // Load members with permissions - const membersResponse = await apiClient.get(`/permissions/${currentClub.value}/members`); + const bust = force ? `?t=${Date.now()}` : ''; + const membersResponse = await apiClient.get(`/permissions/${currentClub.value}/members${bust}`); members.value = membersResponse.data; } catch (err) { console.error('Error loading permissions data:', err); @@ -223,18 +230,37 @@ export default { } }; - const openPermissionsDialog = (member) => { + const openPermissionsDialog = async (member) => { selectedMember.value = member; - // Initialize custom permissions + console.log('Opening dialog for member:', member.user?.email); + console.log('Member permissions from DB:', member.permissions); + + // Load fresh data for this specific member to ensure we have the latest permissions + try { + const membersResponse = await apiClient.get(`/permissions/${currentClub.value}/members?t=${Date.now()}`); + const freshMember = membersResponse.data.find(m => m.userId === member.userId); + if (freshMember) { + selectedMember.value = freshMember; + console.log('Fresh member data:', freshMember.permissions); + } + } catch (err) { + console.error('Error loading fresh member data:', err); + // Continue with existing data if fresh load fails + } + + // Initialize custom permissions from the stored custom permissions, not effective permissions customPermissions.value = {}; for (const resource in permissionStructure.value) { customPermissions.value[resource] = {}; - const effectivePerms = member.effectivePermissions[resource] || {}; + const customPerms = selectedMember.value.permissions || {}; for (const action of permissionStructure.value[resource].actions) { - customPermissions.value[resource][action] = effectivePerms[action] || false; + // Use custom permissions if they exist, otherwise undefined (inherit from role) + customPermissions.value[resource][action] = customPerms[resource]?.[action]; } } + + console.log('Initialized customPermissions:', customPermissions.value); }; const closePermissionsDialog = () => { @@ -244,24 +270,183 @@ export default { const saveCustomPermissions = async () => { try { - await apiClient.put( + // Only send permissions that are explicitly set (not undefined) + const permissionsToSave = {}; + for (const resourceKey in customPermissions.value) { + permissionsToSave[resourceKey] = {}; + for (const action in customPermissions.value[resourceKey]) { + const value = customPermissions.value[resourceKey][action]; + if (value !== undefined) { + permissionsToSave[resourceKey][action] = value; + } + } + // Only include resource if it has at least one permission set + if (Object.keys(permissionsToSave[resourceKey]).length === 0) { + delete permissionsToSave[resourceKey]; + } + } + + console.log('Saving permissions:', permissionsToSave); + + const response = await apiClient.put( `/permissions/${currentClub.value}/user/${selectedMember.value.userId}/permissions`, - { permissions: customPermissions.value } + { permissions: permissionsToSave } ); + console.log('Save response:', response.data); + + // Update local member data immediately + const memberIndex = members.value.findIndex(m => m.userId === selectedMember.value.userId); + if (memberIndex !== -1) { + members.value[memberIndex].permissions = permissionsToSave; + // Recalculate effective permissions + const permService = { getEffectivePermissions: (uc) => { + const rolePerms = getRolePermissions(uc.role); + const customPerms = uc.permissions || {}; + const merged = JSON.parse(JSON.stringify(rolePerms)); + for (const resource in customPerms) { + if (!merged[resource]) merged[resource] = {}; + merged[resource] = { ...merged[resource], ...customPerms[resource] }; + } + return merged; + }}; + members.value[memberIndex].effectivePermissions = permService.getEffectivePermissions(members.value[memberIndex]); + } + closePermissionsDialog(); - await loadData(); + // Hard reload from server to reflect saved values (cache-busting) + await loadData(true); } catch (err) { console.error('Error saving permissions:', err); alert(err.response?.data?.error || 'Fehler beim Speichern der Berechtigungen'); } }; + const togglePermission = (resourceKey, action) => { + const current = customPermissions.value[resourceKey][action]; + const rolePermissions = getRolePermissions(selectedMember.value.role); + const roleValue = rolePermissions[resourceKey]?.[action]; + + // Toggle between: role value -> opposite of role value -> role value + if (current === undefined) { + // Currently using role value, set to opposite + customPermissions.value[resourceKey][action] = !roleValue; + } else { + // Currently overridden, reset to role value + customPermissions.value[resourceKey][action] = undefined; + } + }; + + const resetResource = (resourceKey) => { + for (const action of permissionStructure.value[resourceKey].actions) { + customPermissions.value[resourceKey][action] = undefined; + } + }; + + const resetAll = () => { + for (const resourceKey in permissionStructure.value) { + resetResource(resourceKey); + } + }; + + const stateLabel = (val, resourceKey, action) => { + if (val === true) return 'Erlaubt'; + if (val === false) return 'Verboten'; + + // Show role-based permission + const rolePermissions = getRolePermissions(selectedMember.value.role); + const roleValue = rolePermissions[resourceKey]?.[action]; + return roleValue ? 'Erlaubt' : 'Verboten'; + }; + + const stateClass = (val, resourceKey, action) => { + // If undefined, it's using role value (inherit) + if (val === undefined) { + return 'state-inherit'; + } + + // If explicitly set, show as override + return val === true ? 'state-allow' : 'state-deny'; + }; + const getRoleLabel = (roleValue) => { const role = availableRoles.value.find(r => r.value === roleValue); return role ? role.label : roleValue; }; + const getRolePermissions = (role) => { + // Role permissions mapping (should match backend) + const rolePermissions = { + admin: { + diary: { read: true, write: true, delete: true }, + members: { read: true, write: true, delete: true }, + teams: { read: true, write: true, delete: true }, + schedule: { read: true, write: true, delete: true }, + tournaments: { read: true, write: true, delete: true }, + statistics: { read: true, write: true }, + settings: { read: true, write: true }, + permissions: { read: true, write: true }, + approvals: { read: true, write: true }, + mytischtennis_admin: { read: true, write: true }, + predefined_activities: { read: true, write: true, delete: true } + }, + trainer: { + diary: { read: true, write: true, delete: true }, + members: { read: true, write: true, delete: false }, + teams: { read: true, write: true, delete: false }, + schedule: { read: true, write: false, delete: false }, + tournaments: { read: true, write: true, delete: false }, + statistics: { read: true, write: false }, + settings: { read: false, write: false }, + permissions: { read: false, write: false }, + approvals: { read: false, write: false }, + mytischtennis_admin: { read: false, write: false }, + predefined_activities: { read: true, write: true, delete: true } + }, + team_manager: { + diary: { read: false, write: false, delete: false }, + members: { read: true, write: false, delete: false }, + teams: { read: true, write: true, delete: false }, + schedule: { read: true, write: true, delete: false }, + tournaments: { read: true, write: false, delete: false }, + statistics: { read: true, write: false }, + settings: { read: false, write: false }, + permissions: { read: false, write: false }, + approvals: { read: false, write: false }, + mytischtennis_admin: { read: false, write: false }, + predefined_activities: { read: false, write: false, delete: false } + }, + tournament_manager: { + diary: { read: false, write: false, delete: false }, + members: { read: true, write: false, delete: false }, + teams: { read: false, write: false, delete: false }, + schedule: { read: false, write: false, delete: false }, + tournaments: { read: true, write: true, delete: false }, + statistics: { read: true, write: false }, + settings: { read: false, write: false }, + permissions: { read: false, write: false }, + approvals: { read: false, write: false }, + mytischtennis_admin: { read: false, write: false }, + predefined_activities: { read: false, write: false, delete: false } + }, + member: { + diary: { read: false, write: false, delete: false }, + members: { read: false, write: false, delete: false }, + teams: { read: false, write: false, delete: false }, + schedule: { read: false, write: false, delete: false }, + tournaments: { read: false, write: false, delete: false }, + statistics: { read: true, write: false }, + settings: { read: false, write: false }, + permissions: { read: false, write: false }, + approvals: { read: false, write: false }, + mytischtennis_admin: { read: false, write: false }, + predefined_activities: { read: false, write: false, delete: false } + } + }; + + return rolePermissions[role] || rolePermissions.member; + }; + const getActionLabel = (action) => { const labels = { read: 'Lesen', @@ -297,6 +482,11 @@ export default { openPermissionsDialog, closePermissionsDialog, saveCustomPermissions, + togglePermission, + resetResource, + resetAll, + stateLabel, + stateClass, getRoleLabel, getActionLabel }; @@ -570,6 +760,12 @@ th { padding: 15px; } +.permission-group-header { + display: flex; + justify-content: space-between; + align-items: center; +} + .permission-group h4 { margin: 0 0 12px 0; color: #2c3e50; @@ -582,6 +778,17 @@ th { gap: 10px; } +.permission-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.permission-action-label { + color: #2c3e50; +} + .permission-checkbox { display: flex; align-items: center; @@ -627,5 +834,42 @@ th { .btn-secondary:hover { background-color: #e0e0e0; } + +.btn-reset { + background: #f5f5f5; + border: 1px solid #ddd; + color: #333; + padding: 6px 10px; + border-radius: 4px; + cursor: pointer; +} + +.btn-reset:disabled { + opacity: 0.6; + cursor: default; +} + +.perm-state { + padding: 6px 10px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9em; +} + +.state-inherit { + background: #f5f5f5; + color: #333; +} + +.state-allow { + background: #e8f5e9; + color: #2e7d32; +} + +.state-deny { + background: #ffebee; + color: #c62828; +}