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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user