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.
This commit is contained in:
Torsten Schulz (local)
2025-10-17 11:55:43 +02:00
parent 56f0ce2f27
commit 48bbc8015b
3 changed files with 310 additions and 28 deletions

View File

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

View File

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

View File

@@ -102,21 +102,27 @@
<div class="permissions-grid">
<div v-for="(resource, key) in permissionStructure" :key="key" class="permission-group">
<h4>{{ resource.label }}</h4>
<div class="permission-group-header">
<h4>{{ resource.label }}</h4>
<button class="btn-reset" @click="resetResource(key)" :disabled="isReadOnly">Zurücksetzen</button>
</div>
<div class="permission-actions">
<label v-for="action in resource.actions" :key="action" class="permission-checkbox">
<input
type="checkbox"
v-model="customPermissions[key][action]"
/>
{{ getActionLabel(action) }}
</label>
<div v-for="action in resource.actions" :key="action" class="permission-row">
<span class="permission-action-label">{{ getActionLabel(action) }}</span>
<button
class="perm-state"
:class="stateClass(customPermissions[key][action], key, action)"
@click="togglePermission(key, action)"
:disabled="isReadOnly"
>{{ stateLabel(customPermissions[key][action], key, action) }}</button>
</div>
</div>
</div>
</div>
</div>
<div class="dialog-footer">
<button @click="resetAll" class="btn-secondary" :disabled="isReadOnly">Alle zurücksetzen</button>
<button @click="closePermissionsDialog" class="btn-secondary">Abbrechen</button>
<button @click="saveCustomPermissions" class="btn-primary">Speichern</button>
</div>
@@ -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;
}
</style>