diff --git a/dbcreate.sql b/dbcreate.sql index c79308a..ca4be61 100644 --- a/dbcreate.sql +++ b/dbcreate.sql @@ -367,10 +367,9 @@ INSERT INTO dbo.EventLevels VALUES (4,'error','Error',1), (8,'throw_exception','Exception',0); -INSERT INTO dbo.Plugins VALUES ('SYSTEM',1,'1.0.0'); INSERT INTO dbo.[Role] (Name,Description,RoleType) -VALUES ('ADMIN','System Administrator','SYSTEM'); +VALUES ('ADMIN','System Administrators','SYSTEM'); INSERT INTO dbo.Permission (Scope,Resource,Action) VALUES ('SYSTEM','ALL','ALL'); @@ -493,18 +492,6 @@ JOIN dbo.[Role] r GO --- ======================================================== --- 3. EFFECTIVE ROLES (DEDUPLICATED) --- ======================================================== -CREATE OR ALTER VIEW dbo.vAuthenticationEffectiveRoles AS -SELECT DISTINCT - Authentication_ObjectGUID, - Role_ID, - RoleName -FROM dbo.vAuthenticationRolesExpanded; -GO - - -- ======================================================== -- 4. PERMISSIONS (DETAILED WITH ROLE SOURCE) -- ======================================================== diff --git a/public/javascript/customModal.js b/public/javascript/customModal.js index 8bce03c..2c5b890 100644 --- a/public/javascript/customModal.js +++ b/public/javascript/customModal.js @@ -149,6 +149,9 @@ function slideOutMessage(message) { /** feedbox({ title: `⚠ Upload abbrechen?`, + lock: true, + primary: 'yes', + replace: false, message: `
Es laufen noch aktive Uploads.
Möchtest du wirklich alle abbrechen?
diff --git a/public/javascript/main.js b/public/javascript/main.js index 5ad1e62..e60a2b1 100644 --- a/public/javascript/main.js +++ b/public/javascript/main.js @@ -1033,6 +1033,7 @@ function virtualTable({ }); // Live-Counter + if(!filterConfig || filterConfig.hideCounter) return if(filterState.counterEl) filterState.counterEl.textContent = `${visibleCount} Treffer`; } @@ -1230,9 +1231,13 @@ window.addEventListener('resize', () => { tableEl.style.setProperty('--filter-height', h + 'px'); }); - filterState.counterEl = document.createElement('div'); + if(!filterConfig.hideCounter +) { +filterState.counterEl = document.createElement('div'); filterState.counterEl.className = 'live-counter'; container.appendChild(filterState.counterEl); + } + syncFilterWidth(container); const wrapperResizeObserver = new ResizeObserver(() => { diff --git a/public/javascript/os.js b/public/javascript/os.js index dd6f720..3e37fca 100644 --- a/public/javascript/os.js +++ b/public/javascript/os.js @@ -11,6 +11,7 @@ const restoreIcon = '🗗'; const startBtn = document.getElementById('start-btn'); const startMenu = document.getElementById('start-menu'); +const taskbar = document.getElementById('taskbar'); const windowsContainer = document.getElementById('windows'); const taskbarWindows = document.getElementById('taskbar-windows'); const ctx = new ContextMenu(); @@ -21,9 +22,27 @@ const MAX_PADDING = { left: 4, top: 4, right: 4, bottom: (56 - 4) }; startBtn.addEventListener('click', (evt) => { evt.stopPropagation(); // verhindert sofortiges Schließen + startBtn.classList.toggle('active'); startMenu.classList.toggle('hidden'); }); +function updateStartMenuPosition() { + const height = taskbar.offsetHeight; + startMenu.style.bottom = (height + 5) + 'px'; +} + +const observer = new ResizeObserver(entries => { + for (let entry of entries) { + const height = entry.contentRect.height; + document.documentElement.style.setProperty('--auto-taskbar-height', height + 'px'); + } +}); + +observer.observe(taskbar); + +window.addEventListener('resize', updateStartMenuPosition); +window.addEventListener('load', updateStartMenuPosition); + // Launch app when clicking start menu item document.addEventListener('click', async (e) => { const target = e.target.closest('.start-item'); @@ -31,6 +50,7 @@ document.addEventListener('click', async (e) => { const clickedButton = startBtn.contains(e.target); if(!clickedInsideMenu && !clickedButton) { + startBtn.classList.remove('active'); startMenu.classList.add('hidden'); return; } diff --git a/public/javascript/rbacAPI.js b/public/javascript/rbacAPI.js index f9bf073..e343a87 100644 --- a/public/javascript/rbacAPI.js +++ b/public/javascript/rbacAPI.js @@ -1,95 +1,229 @@ -const vt = virtualTable({ - tableEl: document.querySelector('#rbacUsersTable'), - data: [], - rowHeight: 20, - buffer: 5, - groupKey: 'ObjectSourceName', // optional zum Gruppieren - rowKey: 'ObjectGUID', - filterConfig: { - exceptedColumns: ['Status_ID', 'Anhänge'], - columnModes: { - ID: 'text', Status: 'dropdown', Objekt: 'text', Priorität: 'dropdown', - Erstelldatum: 'text', Gewerk: 'dropdown', Typ: 'dropdown', - Bedarfsmelder: 'text', Bearbeiter: 'text', Genehmiger: 'text', - Status: 'dropdown' - } - }, - customRender: (row, tr) => { - - createTd(tr, - ``, { - styles: { - 'position': 'sticky', - 'left': '0px', - 'width': '20px', - 'z-index': '2' - }, classes: [ - 'text-align:left' - ], onclick: () => { - sendUserEvent('RBAC', `Benutzer ${row['sn'][0].toUpperCase() + row['sn'].slice(1)}, ${row['givenName'][0].toUpperCase() + row['givenName'].slice(1)} gelöscht`, null, 3); - } - }); +const ctx = new ContextMenu(); - createTd(tr, row['ObjectGUID'], { classes: [ 'text-align:left' ], styles: { 'max-width': '100px' }, attributes: { 'data-tooltip': row['ObjectGUID'] } }); - createTd(tr, row['sAMAccountName'], { classes: [ 'text-align:left' ], attributes: { 'data-tooltip': row['sAMAccountName'] } }); - createTd(tr, row['sn'], { classes: [ 'text-align:left' ], attributes: { 'data-tooltip': row['sn'] } }); - createTd(tr, row['givenName'], { classes: [ 'text-align:left' ], attributes: { 'data-tooltip': row['givenName'] } }); - createTd(tr, row['mail'], { classes: [ 'text-align:left' ], attributes: { 'data-tooltip': row['mail'] } }); - createTd(tr, row['active'], { classes: [ 'text-align:center' ] }); - createTd(tr, row['online'], { classes: [ 'text-align:center' ] }); - createTd(tr, row['RoleCount'], { classes: [ 'text-align:center' ] }); - createTd(tr, row['GroupCount'], { classes: [ 'text-align:center' ] }); - createTd(tr, row['ObjectSourceName'], { classes: [ 'text-align:right' ] }); - } - }); +////////////////////////////// +// 🌐 API LAYER +////////////////////////////// - -async function api(url, method = 'GET', body) { +const api = async (url, method = 'GET', body) => { const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined }); - - return res.json(); -} - -async function createUser() { - const name = document.getElementById('newUserName').value; - const sn = name.split('.')[1]; - const givenName = name.split('.')[0]; - const mail = `${name}@test.com`; - - const user = await api('/api/rbac/auth/create', 'POST', { - sAMAccountName: name, - mail: mail, - sn: sn[0].toUpperCase() + sn.slice(1), - givenName: givenName[0].toUpperCase() + givenName.slice(1) - }); - if(user) { - sendUserEvent('RBAC', `Benutzer ${sn[0].toUpperCase() + sn.slice(1)}, ${givenName[0].toUpperCase() + givenName.slice(1)} angelegt`, null, 0); - loadUsers(); + if(res.status >= 400) { + const text = await res.text(); + sendUserEvent('RBAC', `Hoppla, da ist etwas schief gelaufen:\r\n${text}`, null, 4); } + return res.json(); +}; + +////////////////////////////// +// 🧠 RBAC SERVICE LAYER +////////////////////////////// + +const RBAC = { + + // 👤 USERS + loadUsers: async () => (await api('/api/rbac/auth/get', 'POST')) + .map(({ active, online, ...rest }) => ({ + ...rest, + Aktiv: active + })) + .sort((a, b) => a.sAMAccountName.localeCompare(b.sAMAccountName)), + + createUser: (data) => api('/api/rbac/auth/create', 'POST', data), + + deleteUser: (guid) => api(`/api/rbac/auth/${guid}`, 'DELETE'), + + // 👥 GROUPS + loadGroups: () => api('/api/rbac/group/get', 'POST'), + + createGroup: (name) => api('/api/rbac/group/create', 'POST', { name }), + + deleteGroup: (guid) => api(`/api/rbac/group/${guid}`, 'DELETE'), + + // 🔗 ASSIGNMENTS + addUserToGroup: (authGuid, groupGuid) => + api('/api/rbac/group/add-user', 'POST', { authGuid, groupGuid }), + + addUserToRole: (authGuid, roleId) => + api('/api/rbac/role/add-user', 'POST', { authGuid, roleId }), + + addGroupToRole: (groupGuid, roleId) => + api('/api/rbac/role/add-group', 'POST', { groupGuid, roleId }), + + addPermissionToRole: (roleId, permissionId) => + api('/api/rbac/role/add-permission', 'POST', { roleId, permissionId }) + +}; + +////////////////////////////// +// 🖱️ DRAG & DROP +////////////////////////////// + +function createDragZone(el, data) { + el.draggable = true; + el.addEventListener('dragstart', (evt) => { + evt.dataTransfer.setData('application/json', JSON.stringify(data)); + }); } +function createDropZone(el, type, target) { + const targetValue = target.ObjectGUID || target.Role_ID || target.Permission_ID; + let process = { action: null, response: null, failure: false }; + el.addEventListener('dragover', e => e.preventDefault()); + + el.addEventListener('drop', async (e) => { + e.preventDefault(); + + const data = JSON.parse( + e.dataTransfer.getData('application/json') + ); + + if(!targetValue) { + sendUserEvent('RBAC', 'Deinem Drop-Ziel wurde kein ID-Attribut zugewiesen. Frag einfach Manuel ', null, 2); + return; + } + + switch (type) { + case 'group': + process.action = `Du hast den Benutzer der Gruppe hinzugefügt`; + process.response = await RBAC.addUserToGroup(data.ObjectGUID, targetValue); + loadUsers(); + break; + case 'role': + process.action = `Du hast den Benutzer der Rolle hinzugefügt`; + process.response = await RBAC.addUserToRole(data.ObjectGUID, targetValue); + break; + + case 'group-to-role': + process.action = `Du hast die Gruppe der Rolle hinzugefügt`; + process.response = await RBAC.addGroupToRole(data.ObjectGUID, targetValue); + break; + + case 'permission': + process.action = `Du hast den Berechtigung der Rolle hinzugefügt`; + process.response = await RBAC.addPermissionToRole(targetValue, data.ID); + break; + } + + if(process.failure) return; + sendUserEvent('RBAC', process.action, null, 0); + }); +} + +////////////////////////////// +// 📋 TABLE (USERS) +////////////////////////////// + +const vt = virtualTable({ + tableEl: document.querySelector('#rbacUsersTable'), + data: [], + rowHeight: 20, + buffer: 5, + groupKey: 'ObjectSourceName', + rowKey: 'ObjectGUID', + filterConfig: { + hideCounter: true, + exceptedColumns: ['', 'Rollen', 'Gruppen'], + columnModes: { + Aktiv: 'dropdown', + Online: 'dropdown' + } + }, + + customRender: (row, tr) => { + + createDragZone(tr, row); + + tr.addEventListener('contextmenu', (evt) => { + evt.preventDefault(); + + ctx.setItems([ + { + label: "Details", + onClick: () => showAuthDetails(row.ObjectGUID) + } + ]); + + ctx.show(evt.pageX + 5, { y: evt.pageY + 5 }); + }); + + createTd(tr, + ``, { + styles: { + 'position': 'sticky', + 'left': '0px', + 'max-width': '20px', + 'z-index': '2' + }, classes: [ + 'text-align:left' + ], onclick: () => { + if(row['ObjectGUID'] === '00000000-0000-0000-0000-000000000001') return; + deleteUser(row['ObjectGUID'], row['sAMAccountName']); + } + }); + createTd(tr, row['ObjectGUID'], { classes: [ 'text-align:left' ], styles: { 'max-width': '100px' }, attributes: { 'data-tooltip': row['ObjectGUID'] } }); + createTd(tr, row['sAMAccountName'], { classes: [ 'text-align:left' ], attributes: { 'data-tooltip': row['sAMAccountName'] } }); + createTd(tr, row['RoleCount'], { classes: [ 'text-align:center' ] }); + createTd(tr, row['GroupCount'], { classes: [ 'text-align:center' ] }); + createTd(tr, row['sn'], { classes: [ 'text-align:left' ], attributes: { 'data-tooltip': row['sn'] } }); + createTd(tr, row['givenName'], { classes: [ 'text-align:left' ], attributes: { 'data-tooltip': row['givenName'] } }); + createTd(tr, row['mail'], { classes: [ 'text-align:left' ], attributes: { 'data-tooltip': row['mail'] } }); + createTd(tr, row['Aktiv'], { classes: [ 'text-align:center' ] }); + } +}); + +////////////////////////////// +// 📥 LOADERS +////////////////////////////// async function loadUsers() { try { - const users = await api('/api/rbac/auth/get', 'POST'); - if(users) { - vt.source(users); - return; - } - sendUserEvent('RBAC', 'Benutzer konnten nicht geladen', null, 4); - } catch(err) { - writeEventLog(4, 'RBAC', err); + vt.source(await RBAC.loadUsers()); + } catch (err) { + console.error(err); } } +async function loadGroups() { + const container = document.getElementById('rbacGroupContainer'); + container.innerHTML = ''; + + const groups = await RBAC.loadGroups(); + + const fragment = document.createDocumentFragment(); + + groups.forEach(group => { + + const section = document.createElement('section'); + const groupName = document.createElement('span'); + groupName.innerHTML = group.Name; + groupName.dataset.tooltip = group.Name; + groupName.dataset.tooltipMode = 'ellipsis'; + section.append(groupName); + + if(group.ObjectGUID !== '00000000-0000-0000-0000-000000000001') { + const removeButton = document.createElement('div'); + removeButton.className = 'removeButton'; + removeButton.innerHTML = 'X'; + removeButton.onclick = async () => { + await deleteGroup(group.ObjectGUID, group.Name); + }; + section.append(removeButton); + } + + createDropZone(section, 'group', group); + + fragment.appendChild(section); + }); + + container.appendChild(fragment); +} async function createGroup() { const name = document.getElementById('newGroupName').value; @@ -101,76 +235,411 @@ async function createGroup() { loadGroups(); } } -// HIER WEITER - GRUPPEN KARTEN MÜSSEN HÜBSCHER WERDEN. -// BENUTZER UND GRUPPEN KÖNNEN NOCH NICHT GELÖSCHT WERDEN. -// GRUPPEN AUCH OBJECTSOURCE_ID 1? -async function loadGroups() { - try { - const rbacGroupContainer = document.getElementById('rbacGroupContainer'); - rbacGroupContainer.innerHTML = ''; - const groups = await api('/api/rbac/group/get', 'POST'); - if(groups) { - let fragment = document.createDocumentFragment(); - groups.forEach(group => { - const section = document.createElement('section'); - section.innerHTML = `${group.Name}${key}:${value}
`).join(''), +// buttons: { +// yes: { +// text: 'Schließen' +// } +// }, +// lock: true, +// primary: 'yes' +// }) +// } + + +// async function createUser() { +// const name = document.getElementById('newUserName').value; +// if(!name || !name.includes('.')) { +// sendUserEvent('RBAC', 'Der sAMAccountName muss einen Punkt enthaltenMöchtest du den Benutzer ${sAMAccountName} [${guid}] wirklich löschen
+// `, +// buttons: { +// no: { +// text: 'Nein' +// }, +// yes: { +// text: 'Ja', +// onClick: async () => { +// const user = await api(`/api/rbac/auth/${guid}`, 'DELETE', { +// guid +// }); +// if(user) { +// loadUsers(); +// sendUserEvent('RBAC', `Benutzer ${user.sAMAccountName || ''} [${user.ObjectGUID}] gelöscht`, null, 0); +// } +// } +// } +// } +// }); +// } + + +// async function loadUsers() { +// try { +// const users = await api('/api/rbac/auth/get', 'POST'); +// if(users) { +// vt.source(users); +// return; +// } +// sendUserEvent('RBAC', 'Benutzer konnten nicht geladen', null, 4); +// } catch(err) { +// writeEventLog(4, 'RBAC', err); +// } +// } + + +// async function createGroup() { +// const name = document.getElementById('newGroupName').value; +// const group = await api('/api/rbac/group/create', 'POST', { +// name +// }); +// if(group) { +// sendUserEvent('RBAC', `Gruppe ${name} angelegt`, null, 0); +// loadGroups(); +// } +// } + + +// async function deleteGroup(guid, name) { +// feedbox({ +// title: `Gruppe löschen`, +// message: ` +//Möchtest du die Gruppe ${name} [${guid}] wirklich löschen
+// `, +// buttons: { +// no: { +// text: 'Nein' +// }, +// yes: { +// text: 'Ja', +// onClick: async () => { +// const group = await api(`/api/rbac/group/${guid}`, 'DELETE', { +// guid +// }); +// if(group) { +// sendUserEvent('RBAC', `Gruppe ${group.Name || ''} [${group.ObjectGUID}] gelöscht`, null, 0); +// loadGroups(); +// } +// } +// } +// } +// }); +// // const name = document.getElementById('newGroupName').value; +// // const group = await api('/api/rbac/group/create', 'POST', { +// // name +// // }); +// // if(group) { +// // sendUserEvent('RBAC', `Gruppe ${name} angelegt`, null, 0); +// // loadGroups(); +// // } +// } + + +// // HIER WEITER - GRUPPEN KARTEN MÜSSEN HÜBSCHER WERDEN. +// // BENUTZER UND GRUPPEN KÖNNEN NOCH NICHT GELÖSCHT WERDEN. +// // GRUPPEN AUCH OBJECTSOURCE_ID 1? +// async function loadGroups() { +// try { +// const rbacGroupContainer = document.getElementById('rbacGroupContainer'); +// rbacGroupContainer.innerHTML = ''; +// const groups = await api('/api/rbac/group/get', 'POST'); +// if(groups) { +// let fragment = document.createDocumentFragment(); +// groups.forEach(group => { +// const section = document.createElement('section'); + +// createDropZone(section, 'group', group.ObjectGUID); + +// section.innerHTML = `${group.Name}