356 lines
9.7 KiB
JavaScript
356 lines
9.7 KiB
JavaScript
import { db } from '../db/index.js';
|
|
|
|
const LEVEL_PRIORITY = {
|
|
none: 0,
|
|
read: 1,
|
|
full: 2
|
|
};
|
|
|
|
const selectSections = db.prepare(`
|
|
SELECT id, key, title, sort_order, has_navigation_flag
|
|
FROM permission_sections
|
|
ORDER BY sort_order ASC, id ASC
|
|
`);
|
|
|
|
const selectGroups = db.prepare(`
|
|
SELECT id, section_id, key, title, sort_order
|
|
FROM permission_groups
|
|
ORDER BY section_id ASC, sort_order ASC, id ASC
|
|
`);
|
|
|
|
const selectItems = db.prepare(`
|
|
SELECT
|
|
id,
|
|
section_id,
|
|
group_id,
|
|
key,
|
|
label,
|
|
sort_order,
|
|
default_level,
|
|
available_levels,
|
|
is_required
|
|
FROM permission_items
|
|
ORDER BY section_id ASC, COALESCE(group_id, 0) ASC, sort_order ASC, id ASC
|
|
`);
|
|
|
|
const selectDependencies = db.prepare(`
|
|
SELECT permission_id, depends_on_permission_id, required_level
|
|
FROM permission_dependencies
|
|
`);
|
|
|
|
const selectPermissionItemsForMap = db.prepare(`
|
|
SELECT id, key, available_levels, default_level
|
|
FROM permission_items
|
|
`);
|
|
|
|
const selectGroupPermissionValues = db.prepare(`
|
|
SELECT gp.permission_id, gp.level, gp.effective_level, pi.key
|
|
FROM group_permission_values gp
|
|
JOIN permission_items pi ON pi.id = gp.permission_id
|
|
WHERE gp.group_id = ?
|
|
`);
|
|
|
|
const deleteGroupPermissionValuesStmt = db.prepare(`
|
|
DELETE FROM group_permission_values
|
|
WHERE group_id = ?
|
|
`);
|
|
|
|
const insertGroupPermissionValueStmt = db.prepare(`
|
|
INSERT INTO group_permission_values (group_id, permission_id, level, effective_level, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
`);
|
|
|
|
const parseAvailableLevels = (raw) => {
|
|
if (typeof raw !== 'string' || !raw.trim()) {
|
|
return ['full', 'read', 'none'];
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(raw);
|
|
if (Array.isArray(parsed) && parsed.every((entry) => typeof entry === 'string')) {
|
|
return parsed;
|
|
}
|
|
} catch (error) {
|
|
console.warn('⚠️ Konnte available_levels nicht parsen:', error.message);
|
|
}
|
|
return ['full', 'read', 'none'];
|
|
};
|
|
|
|
const getLevelPriority = (level) => {
|
|
if (!level) return 0;
|
|
return LEVEL_PRIORITY[level] ?? 0;
|
|
};
|
|
|
|
let cachedPermissionItems = null;
|
|
|
|
const getPermissionItems = () => {
|
|
if (!cachedPermissionItems) {
|
|
cachedPermissionItems = selectPermissionItemsForMap.all();
|
|
}
|
|
return cachedPermissionItems;
|
|
};
|
|
|
|
const resetPermissionItemsCache = () => {
|
|
cachedPermissionItems = null;
|
|
};
|
|
|
|
export function getPermissionStructure() {
|
|
const sections = selectSections.all();
|
|
const groups = selectGroups.all();
|
|
const items = selectItems.all();
|
|
const dependencies = selectDependencies.all();
|
|
|
|
const itemIdToKey = new Map(items.map((item) => [item.id, item.key]));
|
|
|
|
const dependenciesByItemId = new Map();
|
|
dependencies.forEach((dependency) => {
|
|
const list = dependenciesByItemId.get(dependency.permission_id) || [];
|
|
list.push(dependency);
|
|
dependenciesByItemId.set(dependency.permission_id, list);
|
|
});
|
|
|
|
const groupsBySectionId = new Map();
|
|
groups.forEach((group) => {
|
|
const list = groupsBySectionId.get(group.section_id) || [];
|
|
list.push(group);
|
|
groupsBySectionId.set(group.section_id, list);
|
|
});
|
|
|
|
const itemsBySectionId = new Map();
|
|
const itemsByGroupId = new Map();
|
|
|
|
items.forEach((item) => {
|
|
if (item.group_id) {
|
|
const list = itemsByGroupId.get(item.group_id) || [];
|
|
list.push(item);
|
|
itemsByGroupId.set(item.group_id, list);
|
|
} else {
|
|
const list = itemsBySectionId.get(item.section_id) || [];
|
|
list.push(item);
|
|
itemsBySectionId.set(item.section_id, list);
|
|
}
|
|
});
|
|
|
|
const formatItem = (item) => {
|
|
const availableLevels = parseAvailableLevels(item.available_levels);
|
|
|
|
const dependencyEntries = dependenciesByItemId.get(item.id) || [];
|
|
const formattedDependencies = dependencyEntries
|
|
.map((dependency) => {
|
|
const dependsOnKey = itemIdToKey.get(dependency.depends_on_permission_id);
|
|
if (!dependsOnKey) {
|
|
return null;
|
|
}
|
|
return {
|
|
key: dependsOnKey,
|
|
requiredLevel: dependency.required_level || null
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
|
|
return {
|
|
key: item.key,
|
|
label: item.label,
|
|
sortOrder: item.sort_order ?? 0,
|
|
defaultLevel: item.default_level || 'none',
|
|
availableLevels,
|
|
isRequired: Boolean(item.is_required),
|
|
dependencies: formattedDependencies
|
|
};
|
|
};
|
|
|
|
const formattedSections = sections.map((section) => {
|
|
const sectionItems = (itemsBySectionId.get(section.id) || []).map(formatItem);
|
|
const sectionGroups = (groupsBySectionId.get(section.id) || []).map((group) => ({
|
|
key: group.key,
|
|
title: group.title,
|
|
sortOrder: group.sort_order ?? 0,
|
|
items: (itemsByGroupId.get(group.id) || []).map(formatItem)
|
|
}));
|
|
|
|
return {
|
|
key: section.key,
|
|
title: section.title,
|
|
sortOrder: section.sort_order ?? 0,
|
|
hasNavigation: Boolean(section.has_navigation_flag),
|
|
items: sectionItems,
|
|
groups: sectionGroups
|
|
};
|
|
});
|
|
|
|
return formattedSections;
|
|
}
|
|
|
|
export function getPermissionValuesByGroup(groupId) {
|
|
const numericId = Number(groupId);
|
|
if (!Number.isFinite(numericId) || numericId <= 0) {
|
|
return {};
|
|
}
|
|
|
|
const rows = selectGroupPermissionValues.all(numericId);
|
|
const values = {};
|
|
rows.forEach((row) => {
|
|
if (row?.key && typeof row.level === 'string') {
|
|
values[row.key] = row.level;
|
|
}
|
|
});
|
|
return values;
|
|
}
|
|
|
|
export function clearGroupPermissionValues(groupId) {
|
|
const numericId = Number(groupId);
|
|
if (!Number.isFinite(numericId) || numericId <= 0) {
|
|
return;
|
|
}
|
|
deleteGroupPermissionValuesStmt.run(numericId);
|
|
}
|
|
|
|
export function saveGroupPermissionValues(groupId, values = {}) {
|
|
const numericId = Number(groupId);
|
|
if (!Number.isFinite(numericId) || numericId <= 0) {
|
|
const error = new Error('INVALID_GROUP_ID');
|
|
error.code = 'INVALID_GROUP_ID';
|
|
throw error;
|
|
}
|
|
|
|
if (values === null || typeof values !== 'object' || Array.isArray(values)) {
|
|
const error = new Error('PERMISSION_INVALID_PAYLOAD');
|
|
error.code = 'PERMISSION_INVALID_PAYLOAD';
|
|
throw error;
|
|
}
|
|
|
|
const items = getPermissionItems();
|
|
const itemMap = new Map();
|
|
items.forEach((item) => {
|
|
itemMap.set(item.key, {
|
|
id: item.id,
|
|
key: item.key,
|
|
defaultLevel: item.default_level || 'none',
|
|
availableLevels: parseAvailableLevels(item.available_levels)
|
|
});
|
|
});
|
|
|
|
const entries = Object.entries(values);
|
|
const normalizedEntries = [];
|
|
|
|
entries.forEach(([key, rawValue]) => {
|
|
const item = itemMap.get(key);
|
|
if (!item) {
|
|
const error = new Error('PERMISSION_UNKNOWN_KEY');
|
|
error.code = 'PERMISSION_UNKNOWN_KEY';
|
|
error.permissionKey = key;
|
|
throw error;
|
|
}
|
|
|
|
let level = typeof rawValue === 'string' ? rawValue.trim() : String(rawValue ?? '').trim();
|
|
if (!level) {
|
|
level = 'none';
|
|
}
|
|
|
|
if (!item.availableLevels.includes(level)) {
|
|
const error = new Error('PERMISSION_INVALID_LEVEL');
|
|
error.code = 'PERMISSION_INVALID_LEVEL';
|
|
error.permissionKey = key;
|
|
error.level = level;
|
|
throw error;
|
|
}
|
|
|
|
normalizedEntries.push({ item, level });
|
|
});
|
|
|
|
const runSave = db.transaction(() => {
|
|
deleteGroupPermissionValuesStmt.run(numericId);
|
|
normalizedEntries.forEach(({ item, level }) => {
|
|
if (level === (item.defaultLevel || 'none')) {
|
|
return;
|
|
}
|
|
insertGroupPermissionValueStmt.run(numericId, item.id, level, level);
|
|
});
|
|
});
|
|
|
|
runSave();
|
|
resetPermissionItemsCache();
|
|
|
|
return getPermissionValuesByGroup(numericId);
|
|
}
|
|
|
|
export function getDefaultPermissionMap() {
|
|
const items = getPermissionItems();
|
|
const defaults = {};
|
|
items.forEach((item) => {
|
|
defaults[item.key] = item.default_level || 'none';
|
|
});
|
|
return defaults;
|
|
}
|
|
|
|
export function getSuperuserPermissionMap() {
|
|
const items = getPermissionItems();
|
|
const result = {};
|
|
|
|
items.forEach((item) => {
|
|
const available = parseAvailableLevels(item.available_levels);
|
|
let chosen = 'full';
|
|
if (!available.includes('full')) {
|
|
chosen = available.reduce(
|
|
(best, candidate) =>
|
|
getLevelPriority(candidate) > getLevelPriority(best) ? candidate : best,
|
|
'none'
|
|
);
|
|
}
|
|
result[item.key] = chosen;
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
export function getEffectivePermissionsForGroup(groupId) {
|
|
const defaults = getDefaultPermissionMap();
|
|
const numericId = Number(groupId);
|
|
if (!Number.isFinite(numericId) || numericId <= 0) {
|
|
return defaults;
|
|
}
|
|
|
|
const overrides = getPermissionValuesByGroup(numericId);
|
|
const map = { ...defaults };
|
|
|
|
Object.keys(overrides).forEach((key) => {
|
|
const value = overrides[key];
|
|
if (typeof value === 'string') {
|
|
map[key] = value;
|
|
}
|
|
});
|
|
|
|
return map;
|
|
}
|
|
|
|
export function getEffectivePermissionsForGroups(groupIds = []) {
|
|
const defaults = getDefaultPermissionMap();
|
|
const finalMap = { ...defaults };
|
|
|
|
const iterableIds = Array.isArray(groupIds) ? groupIds : [];
|
|
iterableIds
|
|
.map((value) => Number(value))
|
|
.filter((value) => Number.isFinite(value) && value > 0)
|
|
.forEach((groupId) => {
|
|
const map = getEffectivePermissionsForGroup(groupId);
|
|
Object.entries(map).forEach(([key, level]) => {
|
|
if (getLevelPriority(level) > getLevelPriority(finalMap[key] ?? 'none')) {
|
|
finalMap[key] = level;
|
|
}
|
|
});
|
|
});
|
|
|
|
return finalMap;
|
|
}
|
|
|
|
export function hasRequiredPermission(permissions = {}, permissionKey, requiredLevel = 'full') {
|
|
if (!permissionKey) {
|
|
return true;
|
|
}
|
|
if (requiredLevel === 'none' || requiredLevel === null || requiredLevel === undefined) {
|
|
return true;
|
|
}
|
|
const current = typeof permissions[permissionKey] === 'string' ? permissions[permissionKey] : 'none';
|
|
const required = typeof requiredLevel === 'string' ? requiredLevel : 'full';
|
|
return getLevelPriority(current) >= getLevelPriority(required);
|
|
}
|