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