Initial commit
This commit is contained in:
302
WebRDM/js/app.js
Normal file
302
WebRDM/js/app.js
Normal file
@@ -0,0 +1,302 @@
|
||||
window.folders = []; window.connections = []; window.protocols = []; window.templates = []; window.users = [];
|
||||
window.appSettings = {}; window.securityDashboardUrl = "https://threatmap.fortiguard.com/";
|
||||
window.currentUser = { role: 'viewer', username: '' };
|
||||
window.categories = []; window.scripts = []; window.auditLogs = [];
|
||||
window.networkInstance = null; window.mapLabelMode = window.mapLabelMode || 'name';
|
||||
|
||||
window.isMapPage = document.getElementById('mynetwork') !== null;
|
||||
|
||||
// --- UTILS & UI ---
|
||||
window.initTheme = function() {
|
||||
const t = localStorage.getItem('rdm_theme') || 'light'; document.documentElement.setAttribute('data-theme', t);
|
||||
if(document.getElementById('theme-icon')) { document.getElementById('theme-icon').className = t === 'dark' ? 'bi bi-sun' : 'bi bi-moon'; }
|
||||
return t;
|
||||
};
|
||||
window.toggleTheme = function() {
|
||||
const t = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', t); localStorage.setItem('rdm_theme', t);
|
||||
if(document.getElementById('theme-icon')) { document.getElementById('theme-icon').className = t === 'dark' ? 'bi bi-sun' : 'bi bi-moon'; }
|
||||
if (window.isMapPage) window.drawMap();
|
||||
};
|
||||
window.showToast = function(msg) { const toast = document.getElementById("toast"); if(!toast) return; toast.innerText = msg; toast.className = "show"; setTimeout(() => { toast.className = toast.className.replace("show", ""); }, 3000); };
|
||||
window.copyToClipboard = function(text) { if(!text) return; navigator.clipboard.writeText(text).then(() => window.showToast("In die Zwischenablage kopiert!")); };
|
||||
window.togglePwd = function(id, realPwd) { const el = document.getElementById(id); const icon = document.getElementById('icon-' + id); if(el.innerText === '••••••••') { el.innerText = realPwd; icon.classList.replace('bi-eye', 'bi-eye-slash'); } else { el.innerText = '••••••••'; icon.classList.replace('bi-eye-slash', 'bi-eye'); } };
|
||||
window.switchTab = function(tabId) { document.querySelectorAll('.tab-view').forEach(el => el.style.display = 'none'); document.querySelectorAll('.dev-tab').forEach(el => el.classList.remove('active')); document.getElementById('view-' + tabId).style.display = 'block'; document.getElementById('tab-btn-' + tabId).classList.add('active'); };
|
||||
window.getProtoInfo = function(type) { let p = window.protocols.find(x => x.proto_key === type); return p ? p : {icon: 'bi-link', name: type}; };
|
||||
|
||||
// NEU: Erweiterter Generator mit Filter
|
||||
window.generatePassword = function() {
|
||||
let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+~|}{[]:;?><,./-=";
|
||||
const exclude = window.appSettings.pwd_exclude_chars || "";
|
||||
|
||||
if(exclude) {
|
||||
for(let i=0; i<exclude.length; i++) {
|
||||
chars = chars.split(exclude[i]).join('');
|
||||
}
|
||||
}
|
||||
// Fallback falls ein Spaßvogel alle existierenden Zeichen ausschließt
|
||||
if(chars.length === 0) chars = "aB1!";
|
||||
|
||||
let pw = "";
|
||||
let randomVals = new Uint32Array(16);
|
||||
window.crypto.getRandomValues(randomVals);
|
||||
for(let i=0; i<16; i++) pw += chars[randomVals[i] % chars.length];
|
||||
|
||||
const pwField = document.getElementById('f_pass');
|
||||
pwField.type = 'text';
|
||||
pwField.value = pw;
|
||||
};
|
||||
|
||||
window.showPasswordHistory = async function(id) {
|
||||
const res = await (await fetch('api.php?action=password_history&id=' + id)).json();
|
||||
if(res.length === 0) { window.showToast("Noch keine Historie für dieses Asset vorhanden."); return; }
|
||||
let rows = res.map(h => `<tr><td class="text-muted-custom small">${h.changed_at}</td><td style="font-family:monospace;">••••••••</td><td><button class="btn btn-sm btn-outline-secondary" onclick="window.copyToClipboard('${h.password.replace(/'/g, "\\'")}')"><i class="bi bi-copy"></i> Kopieren</button></td></tr>`).join('');
|
||||
|
||||
const modalHtml = `<div class="modal fade" id="historyModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content" style="background-color: var(--bg-card); color: var(--text-main); border: 1px solid var(--border-color);"><div class="modal-header border-bottom-0"><h5 class="modal-title"><i class="bi bi-clock-history text-primary"></i> Passwort-Historie (Letzte 10)</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><table class="table table-sm text-white"><thead><tr><th>Geändert am</th><th>Passwort</th><th>Aktion</th></tr></thead><tbody>${rows}</tbody></table></div></div></div></div>`;
|
||||
const oldModal = document.getElementById('historyModal'); if(oldModal) oldModal.remove(); document.body.insertAdjacentHTML('beforeend', modalHtml); new bootstrap.Modal(document.getElementById('historyModal')).show();
|
||||
};
|
||||
|
||||
// --- AUTHENTICATION ---
|
||||
window.checkSession = async function() {
|
||||
try {
|
||||
const res = await (await fetch('api.php?action=session')).json();
|
||||
if(res.status === 'active') { window.currentUser.role = res.role; window.currentUser.username = res.username; if(document.getElementById('user-badge')) document.getElementById('user-badge').innerText = res.username; document.getElementById('login-overlay').style.display = 'none'; window.loadData(); }
|
||||
} catch(e) { console.error("Session Check Error:", e); }
|
||||
};
|
||||
window.doLogin = async function() {
|
||||
const u = document.getElementById('l_user').value; const p = document.getElementById('l_pass').value;
|
||||
try {
|
||||
const res = await fetch('api.php?action=login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({username: u, password: p}) });
|
||||
if(res.ok) {
|
||||
const data = await res.json();
|
||||
if(data.status === '2fa_required') { document.getElementById('step-login').classList.add('d-none'); document.getElementById('step-2fa').classList.remove('d-none'); }
|
||||
else { window.currentUser.role = data.role; window.currentUser.username = data.username; if(document.getElementById('user-badge')) document.getElementById('user-badge').innerText = data.username; document.getElementById('login-overlay').style.display = 'none'; window.loadData(); }
|
||||
} else { document.getElementById('login-error').innerText = "Zugangsdaten falsch."; document.getElementById('login-error').classList.remove('d-none'); }
|
||||
} catch (e) { document.getElementById('login-error').innerText = "Verbindungsfehler zur API."; document.getElementById('login-error').classList.remove('d-none'); }
|
||||
};
|
||||
window.verify2FA = async function() {
|
||||
const code = document.getElementById('l_2fa').value; const res = await fetch('api.php?action=verify_2fa', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({code: code}) });
|
||||
if(res.ok) { const data = await res.json(); window.currentUser.role = data.role; window.currentUser.username = data.username; if(document.getElementById('user-badge')) document.getElementById('user-badge').innerText = data.username; document.getElementById('login-overlay').style.display = 'none'; window.loadData(); }
|
||||
else { document.getElementById('login-error').innerText = "Code falsch."; document.getElementById('login-error').classList.remove('d-none'); }
|
||||
};
|
||||
window.doLogout = async function() { await fetch('api.php?action=logout'); location.reload(); };
|
||||
|
||||
// --- DATA LOADING & ROUTING ---
|
||||
window.loadData = async function() {
|
||||
try {
|
||||
window.folders = await (await fetch('api.php?action=folders')).json(); window.connections = await (await fetch('api.php?action=connections')).json();
|
||||
window.protocols = await (await fetch('api.php?action=protocols')).json(); window.appSettings = await (await fetch('api.php?action=settings')).json();
|
||||
window.categories = await (await fetch('api.php?action=categories')).json(); window.scripts = await (await fetch('api.php?action=scripts')).json();
|
||||
if(window.currentUser.role === 'admin') { window.users = await (await fetch('api.php?action=users')).json(); window.auditLogs = await (await fetch('api.php?action=audit')).json(); }
|
||||
window.securityDashboardUrl = window.appSettings.dashboard_url || "https://threatmap.fortiguard.com/";
|
||||
if(window.isMapPage) { window.drawMap(); } else { window.renderSidebar(); window.showHomeDashboard(); }
|
||||
} catch(e) { console.error("Load Data Error", e); }
|
||||
};
|
||||
|
||||
window.showHomeDashboard = function() {
|
||||
if(window.isMapPage) return;
|
||||
document.querySelectorAll('.connection-item').forEach(el => el.classList.remove('active'));
|
||||
let totalAssets = window.connections ? window.connections.length : 0; let totalScripts = window.scripts ? window.scripts.length : 0; let foldersCount = window.folders ? window.folders.length : 0;
|
||||
let assetCounts = {}; if (window.connections) { window.connections.forEach(c => { if(c.asset_type) assetCounts[c.asset_type] = (assetCounts[c.asset_type] || 0) + 1; }); }
|
||||
let breakdownHtml = Object.entries(assetCounts).map(([k,v]) => `<span class="badge bg-secondary me-2 mb-2 fs-6 py-2 px-3 border" style="border-color: rgba(255,255,255,0.1)!important;"><i class="bi bi-hdd-stack"></i> ${k}: ${v}</span>`).join('');
|
||||
let kumaItems = window.connections ? window.connections.filter(c => c.kuma_url) : []; let kumaHtml = '';
|
||||
if(kumaItems.length > 0) {
|
||||
kumaHtml = `<h4 class="mt-5 mb-3 border-bottom pb-2" style="border-color: var(--border-color)!important;"><i class="bi bi-heart-pulse text-danger"></i> Wall of Health</h4><div class="row g-3">`;
|
||||
kumaItems.forEach(c => { let iframeOrImg = c.kuma_url.includes('/badge/') ? `<img src="${c.kuma_url}" style="max-width:100%; border-radius:4px;">` : `<iframe src="${c.kuma_url}" style="width:100%; height:45px; border:none; border-radius:4px; overflow:hidden; pointer-events: none;" scrolling="no"></iframe>`; kumaHtml += `<div class="col-xl-2 col-lg-3 col-md-4 col-sm-6"><div class="p-2 border rounded text-center shadow-sm" style="background:var(--bg-card); border-color:var(--border-color)!important;"><div class="text-truncate small fw-bold mb-2 cursor-pointer" onclick="window.showConnectionDetails(${JSON.stringify(c).replace(/"/g, '"')})" style="color:var(--primary-color); cursor:pointer;">${c.name}</div>${iframeOrImg}</div></div>`; });
|
||||
kumaHtml += `</div>`;
|
||||
}
|
||||
document.getElementById('main-area').innerHTML = `<div class="w-100 h-100 overflow-auto p-4 p-md-5"><h3 class="mb-4 fw-bold"><i class="bi bi-speedometer2 text-primary"></i> System Dashboard</h3><div class="row g-4 mb-5"><div class="col-md-4"><div class="p-4 border rounded text-center shadow-sm h-100" style="background:var(--bg-card); border-color:var(--border-color)!important;"><div class="fs-1 fw-bold text-primary mb-2">${totalAssets}</div><div class="text-muted-custom fw-semibold text-uppercase" style="letter-spacing: 1px; font-size: 0.8rem;">Verbindungen / Assets</div></div></div><div class="col-md-4"><div class="p-4 border rounded text-center shadow-sm h-100" style="background:var(--bg-card); border-color:var(--border-color)!important;"><div class="fs-1 fw-bold text-info mb-2">${totalScripts}</div><div class="text-muted-custom fw-semibold text-uppercase" style="letter-spacing: 1px; font-size: 0.8rem;">Skripte / Snippets</div></div></div><div class="col-md-4"><div class="p-4 border rounded text-center shadow-sm h-100" style="background:var(--bg-card); border-color:var(--border-color)!important;"><div class="fs-1 fw-bold text-warning mb-2">${foldersCount}</div><div class="text-muted-custom fw-semibold text-uppercase" style="letter-spacing: 1px; font-size: 0.8rem;">Ordnerstrukturen</div></div></div></div>${breakdownHtml ? `<div class="mb-4 p-4 border rounded shadow-sm" style="background:var(--bg-card); border-color:var(--border-color)!important;"><h5 class="mb-3 text-muted-custom"><i class="bi bi-pie-chart"></i> Hardware-Inventar</h5><div class="d-flex flex-wrap">${breakdownHtml}</div></div>` : ''}${kumaHtml}</div>`;
|
||||
};
|
||||
|
||||
window.renderSidebar = function() {
|
||||
if(window.isMapPage) return;
|
||||
const sidebar = document.getElementById('sidebar-content'); sidebar.innerHTML = '';
|
||||
if(window.currentUser.role === 'admin') {
|
||||
sidebar.innerHTML += `<div class="px-3 pb-2 border-bottom mb-2" style="border-color: var(--border-color)!important;"><div class="d-flex justify-content-between"><button class="btn btn-sm btn-outline-secondary" onclick="window.openNewFolderModal()"><i class="bi bi-folder-plus"></i></button><div class="dropdown flex-grow-1 ms-2"><button class="btn btn-sm btn-primary w-100" data-bs-toggle="dropdown" aria-expanded="false"><i class="bi bi-plus-lg"></i> Neu</button><ul class="dropdown-menu shadow"><li><a class="dropdown-item" href="#" onclick="window.showConnectionForm()"><i class="bi bi-hdd-network text-primary"></i> Verbindung / Asset</a></li><li><a class="dropdown-item" href="#" onclick="window.showScriptForm()"><i class="bi bi-file-earmark-code text-info"></i> Skript / Snippet</a></li></ul></div></div></div>`;
|
||||
}
|
||||
const tree = {}; window.folders.forEach(f => { const parts = f.split('/'); let current = tree; let path = ""; parts.forEach(p => { path = path ? path + "/" + p : p; if(!current[p]) current[p] = { fullPath: path, children: {} }; current = current[p].children; }); });
|
||||
for(let rootName in tree) { window.renderFolderNode(rootName, tree[rootName], sidebar, 0); }
|
||||
};
|
||||
|
||||
window.renderFolderNode = function(name, node, container, depth) {
|
||||
const wrapper = document.createElement('div'); const folderDiv = document.createElement('div');
|
||||
folderDiv.className = `folder-header px-2 search-item depth-${depth > 2 ? 2 : depth}`;
|
||||
folderDiv.innerHTML = `<i class="bi bi-chevron-down toggle-icon" style="cursor:pointer; font-size: 0.8rem; margin-right: 5px;"></i> <i class="bi bi-folder-fill text-warning"></i> ${name}`;
|
||||
wrapper.appendChild(folderDiv);
|
||||
const childrenContainer = document.createElement('div'); childrenContainer.className = "folder-children";
|
||||
folderDiv.querySelector('.toggle-icon').onclick = (e) => { e.stopPropagation(); childrenContainer.classList.toggle('d-none'); e.target.classList.toggle('bi-chevron-down'); e.target.classList.toggle('bi-chevron-right'); };
|
||||
for(let childName in node.children) { window.renderFolderNode(childName, node.children[childName], childrenContainer, depth + 1); }
|
||||
window.connections.filter(c => c.folder_name === node.fullPath && !c.parent_id).forEach(item => { window.renderConnectionItem(item, childrenContainer, depth + 1); });
|
||||
window.scripts.filter(s => s.folder_name === node.fullPath).forEach(script => { window.renderScriptItem(script, childrenContainer, depth + 1); });
|
||||
wrapper.appendChild(childrenContainer); container.appendChild(wrapper);
|
||||
};
|
||||
|
||||
window.renderConnectionItem = function(item, container, depth) {
|
||||
const itemDiv = document.createElement('div'); itemDiv.setAttribute('data-search', `${item.name} ${item.host} ${item.tags} ${item.asset_type} ${item.hardware}`.toLowerCase()); itemDiv.className = `connection-item depth-${depth > 2 ? 2 : depth} search-item`; itemDiv.innerHTML = `<i class="bi ${window.getProtoInfo(item.type).icon} text-primary"></i> <span class="text-truncate">${item.name}</span>`; itemDiv.onclick = () => { document.querySelectorAll('.connection-item').forEach(el => el.classList.remove('active')); itemDiv.classList.add('active'); window.showConnectionDetails(item); }; container.appendChild(itemDiv); window.connections.filter(c => c.parent_id == item.id).forEach(child => window.renderConnectionItem(child, container, depth + 1));
|
||||
};
|
||||
|
||||
window.renderScriptItem = function(script, container, depth) {
|
||||
const itemDiv = document.createElement('div'); itemDiv.setAttribute('data-search', `${script.name} ${script.language} script`.toLowerCase()); itemDiv.className = `connection-item depth-${depth > 2 ? 2 : depth} search-item`;
|
||||
let icon = 'bi-file-earmark-text'; if(script.language === 'bash') icon = 'bi-terminal'; if(script.language === 'powershell') icon = 'bi-windows'; if(script.language === 'python') icon = 'bi-filetype-py'; if(script.language === 'sql') icon = 'bi-filetype-sql';
|
||||
itemDiv.innerHTML = `<i class="bi ${icon} text-info"></i> <span class="text-truncate" style="font-style: italic;">${script.name}</span>`; itemDiv.onclick = () => { document.querySelectorAll('.connection-item').forEach(el => el.classList.remove('active')); itemDiv.classList.add('active'); window.showScriptDetails(script); }; container.appendChild(itemDiv);
|
||||
};
|
||||
|
||||
window.filterSidebar = function() { const term = document.getElementById('searchInput').value.toLowerCase(); document.querySelectorAll('.search-item').forEach(item => { if(item.classList.contains('folder-header')) { item.style.display = item.innerText.toLowerCase().includes(term) ? '' : 'none'; } else { item.style.display = item.getAttribute('data-search').includes(term) ? '' : 'none'; } }); };
|
||||
|
||||
window.openNewFolderModal = function() {
|
||||
if(window.currentUser.role !== 'admin') return; let folderOptions = '<option value="">-- Kein Überordner (Hauptebene) --</option>'; window.folders.forEach(f => folderOptions += `<option value="${f}">${f}</option>`);
|
||||
const modalHtml = `<div class="modal fade" id="folderModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content" style="background-color: var(--bg-card); color: var(--text-main); border: 1px solid var(--border-color);"><div class="modal-header border-bottom-0"><h5 class="modal-title"><i class="bi bi-folder-plus text-primary"></i> Neuer Ordner</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="mb-3"><label class="form-label text-muted-custom small">In welchem Überordner? (Optional)</label><select id="new_folder_parent" class="form-select">${folderOptions}</select></div><div class="mb-3"><label class="form-label text-muted-custom small">Ordnername</label><input type="text" id="new_folder_name" class="form-control" placeholder="z.B. IT-Infrastruktur oder Rack 1"></div></div><div class="modal-footer border-top-0"><button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button><button type="button" class="btn btn-primary" onclick="window.saveNewFolder()">Speichern</button></div></div></div></div>`;
|
||||
const oldModal = document.getElementById('folderModal'); if(oldModal) oldModal.remove(); document.body.insertAdjacentHTML('beforeend', modalHtml); new bootstrap.Modal(document.getElementById('folderModal')).show();
|
||||
};
|
||||
window.saveNewFolder = async function() { const parent = document.getElementById('new_folder_parent').value; const name = document.getElementById('new_folder_name').value.trim(); if(!name) return; const fullName = parent ? `${parent}/${name}` : name; await fetch('api.php?action=folders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({name: fullName}) }); bootstrap.Modal.getInstance(document.getElementById('folderModal')).hide(); await window.loadData(); };
|
||||
|
||||
window.showScriptForm = function(editItem = null) {
|
||||
if(window.currentUser.role !== 'admin' || window.isMapPage) return;
|
||||
const isEdit = editItem !== null; let folderOptions = '<option value="">-- Hauptebene (Kein Ordner) --</option>'; window.folders.forEach(f => folderOptions += `<option value="${f}" ${isEdit && editItem.folder_name === f ? 'selected' : ''}>📁 ${f}</option>`);
|
||||
const langs = ['bash', 'powershell', 'python', 'sql', 'text']; let langOptions = langs.map(l => `<option value="${l}" ${isEdit && editItem.language === l ? 'selected' : ''}>${l.toUpperCase()}</option>`).join('');
|
||||
document.getElementById('main-area').innerHTML = `<div class="w-100 h-100 overflow-auto p-4"><div class="dashboard-card mx-auto" style="max-width: 900px;"><h4 class="mb-4"><i class="bi bi-file-earmark-code text-info"></i> ${isEdit ? 'Skript bearbeiten' : 'Neues Skript'}</h4><input type="hidden" id="f_script_id" value="${isEdit ? editItem.id : ''}"><div class="row mb-3"><div class="col-md-6"><label class="form-label text-muted-custom small">Speicherort (Ordner)</label><select id="f_script_folder" class="form-select">${folderOptions}</select></div><div class="col-md-6"><label class="form-label text-muted-custom small">Sprache</label><select id="f_script_lang" class="form-select">${langOptions}</select></div></div><div class="mb-3"><label class="form-label text-muted-custom small">Name / Beschreibung</label><input type="text" id="f_script_name" class="form-control" placeholder="z.B. Nginx Config Reset" value="${isEdit ? editItem.name : ''}"></div><div class="mb-4"><label class="form-label text-muted-custom small">Quellcode / Befehle</label><textarea id="f_script_body" class="form-control" rows="15" style="font-family: monospace; background: var(--bg-body); color: var(--text-main);">${isEdit ? editItem.script_body : ''}</textarea></div><div class="d-flex gap-2 justify-content-end">${isEdit ? `<button class="btn btn-outline-secondary" onclick="window.showScriptDetails(${JSON.stringify(editItem).replace(/"/g, '"')})">Abbrechen</button>` : ''}<button class="btn btn-primary px-4" onclick="window.saveScript()"><i class="bi bi-floppy"></i> Speichern</button></div></div></div>`;
|
||||
};
|
||||
window.saveScript = async function() { if(window.currentUser.role !== 'admin') return; const data = { id: document.getElementById('f_script_id').value || null, folder_name: document.getElementById('f_script_folder').value || null, name: document.getElementById('f_script_name').value, language: document.getElementById('f_script_lang').value, script_body: document.getElementById('f_script_body').value }; if(!data.name) return alert("Bitte einen Namen vergeben!"); await fetch('api.php?action=scripts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await window.loadData(); window.showHomeDashboard(); };
|
||||
window.showScriptDetails = function(item) {
|
||||
if(window.isMapPage) return; let adminActions = ''; if (window.currentUser.role === 'admin') { adminActions = `<div class="dev-action-group mt-4"><div class="dev-action-title">Bearbeiten</div><div class="dev-action-btn" onclick="window.showScriptForm(${JSON.stringify(item).replace(/"/g, '"')})"><i class="bi bi-pencil text-success" style="font-size:1.2rem;"></i><span style="font-size:0.8rem;">Editieren</span></div><div class="dev-action-btn mt-2" onclick="window.deleteScript(${item.id})"><i class="bi bi-trash text-danger" style="font-size:1.2rem;"></i><span style="font-size:0.8rem;">Löschen</span></div></div>`; }
|
||||
const safeContent = String(item.script_body).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
document.getElementById('main-area').innerHTML = `<div class="dev-action-sidebar"><div class="dev-action-group"><div class="dev-action-title">Aktionen</div><div class="dev-action-btn" onclick="window.copyToClipboard(document.getElementById('script-content').innerText)"><i class="bi bi-clipboard text-primary"></i><span>Code Kopieren</span></div></div>${adminActions}</div><div class="dev-main-view"><div class="dev-header"><i class="bi bi-file-earmark-code text-info" style="font-size: 2.5rem;"></i><div><h4 class="m-0 fw-bold">${item.name}</h4><div class="text-muted-custom small">Snippet: ${item.language.toUpperCase()}</div></div></div><div class="dev-content p-0"><pre style="background: var(--bg-body); padding: 20px; border-radius: 8px; border: 1px solid var(--border-color); overflow-x: auto; margin: 0; min-height: 400px;"><code id="script-content" style="font-family: Consolas, monospace; color: var(--text-main); font-size: 0.9rem;">${safeContent}</code></pre></div></div>`;
|
||||
};
|
||||
window.deleteScript = async function(id) { if(window.currentUser.role !== 'admin') return; if(!confirm("Skript wirklich löschen?")) return; await fetch('api.php?action=delete_script', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({id: id}) }); await window.loadData(); window.showHomeDashboard(); };
|
||||
|
||||
window.showConnectionForm = function(editItem = null) {
|
||||
if(window.currentUser.role !== 'admin' || window.isMapPage) return; if(window.folders.length === 0 && !editItem) { alert("Bitte erstelle zuerst einen Ordner!"); return; }
|
||||
const isEdit = editItem !== null; let protoOptions = window.protocols.map(p => `<option value="${p.proto_key}" ${isEdit && editItem.type === p.proto_key ? 'selected' : ''}>${p.name}</option>`).join(''); let folderOptions = window.folders.map(f => `<option value="folder:${f}" ${isEdit && editItem.folder_name === f && !editItem.parent_id ? 'selected' : ''}>📁 ${f}</option>`).join(''); let connOptions = window.connections.filter(c => !isEdit || c.id !== editItem.id).map(c => `<option value="conn:${c.id}" ${isEdit && editItem.parent_id == c.id ? 'selected' : ''}>🔗 ${c.name}</option>`).join(''); let uplinkOptions = `<option value="">-- Kein physikalischer Uplink --</option>`; uplinkOptions += window.connections.filter(c => !isEdit || c.id !== editItem.id).map(c => `<option value="${c.id}" ${isEdit && editItem.uplink_id == c.id ? 'selected' : ''}>${c.name}</option>`).join(''); let catOptions = `<option value="">-- Bitte wählen (Bild in img/DeviceCategories ablegen) --</option>`; window.categories.forEach(cat => { catOptions += `<option value="${cat.name}" ${isEdit && editItem.asset_type === cat.name ? 'selected' : ''}>${cat.name}</option>`; });
|
||||
|
||||
document.getElementById('main-area').innerHTML = `<div class="w-100 h-100 overflow-auto p-4"><div class="dashboard-card mx-auto" style="max-width: 800px;"><h4 class="mb-4"><i class="bi ${isEdit ? 'bi-pencil-square' : 'bi-plus-circle'} text-primary"></i> ${isEdit ? 'Eigenschaften bearbeiten' : 'Neuer Eintrag'}</h4><input type="hidden" id="f_conn_id" value="${isEdit ? editItem.id : ''}"><div class="mb-3"><label class="form-label text-muted-custom small">Logischer Zielort</label><select id="f_target" class="form-select"><optgroup label="Ordner">${folderOptions}</optgroup><optgroup label="Unterverbindung">${connOptions}</optgroup></select></div><div class="p-3 border rounded mb-4" style="border-color: var(--border-color)!important;"><h6 class="mb-3 text-primary"><i class="bi bi-hdd-network"></i> Verbindung & System</h6><div class="row"><div class="col-md-6 mb-3"><label class="form-label text-muted-custom small">Name</label><input type="text" id="f_conn_name" class="form-control" value="${isEdit ? editItem.name : ''}"></div><div class="col-md-6 mb-3"><label class="form-label text-muted-custom small">Protokoll</label><select id="f_type" class="form-select">${protoOptions}</select></div></div><div class="row"><div class="col-md-8 mb-3"><label class="form-label text-muted-custom small">Host/IP</label><input type="text" id="f_host" class="form-control" value="${isEdit ? editItem.host : ''}"></div><div class="col-md-4 mb-3"><label class="form-label text-muted-custom small">Port</label><input type="text" id="f_port" class="form-control" value="${isEdit ? editItem.port : ''}"></div></div><div class="row"><div class="col-md-6 mb-3"><label class="form-label text-muted-custom small">Benutzername</label><input type="text" id="f_user" class="form-control" value="${isEdit ? editItem.username : ''}"></div>
|
||||
<div class="col-md-6 mb-3"><label class="form-label text-muted-custom small">Passwort</label><div class="input-group"><input type="password" id="f_pass" class="form-control" value="${isEdit ? editItem.password : ''}"><button class="btn btn-outline-secondary" type="button" onclick="window.generatePassword()" title="Passwort würfeln"><i class="bi bi-dice-5"></i></button></div></div>
|
||||
</div><div class="mb-3"><label class="form-label text-muted-custom small">Tags (z.B. #linux)</label><input type="text" id="f_tags" class="form-control" value="${isEdit && editItem.tags ? editItem.tags : ''}"></div><div class="mb-1"><label class="form-label text-muted-custom small">Uptime Kuma Badge URL</label><input type="text" id="f_kuma" class="form-control" value="${isEdit && editItem.kuma_url ? editItem.kuma_url : ''}"></div></div><div class="p-3 border rounded mb-4" style="border-color: var(--border-color)!important;"><h6 class="mb-3 text-primary"><i class="bi bi-diagram-3"></i> Topologie & Port-Mapping</h6><div class="row mb-3 pb-3 border-bottom" style="border-color: var(--border-color)!important;"><div class="col-md-4"><label class="form-label text-muted-custom small">Uplink (hängt an...)</label><select id="f_uplink_id" class="form-select">${uplinkOptions}</select></div><div class="col-md-4"><label class="form-label text-muted-custom small">Lokaler Port (an diesem Gerät)</label><input type="text" id="f_local_port" class="form-control" placeholder="z.B. eth0, Port 1" value="${isEdit && editItem.local_port ? editItem.local_port : ''}"></div><div class="col-md-4"><label class="form-label text-muted-custom small">Uplink Port (am Zielgerät)</label><input type="text" id="f_uplink_port" class="form-control" placeholder="z.B. Port 24" value="${isEdit && editItem.uplink_port ? editItem.uplink_port : ''}"></div></div><h6 class="mb-3 text-primary mt-3"><i class="bi bi-box-seam"></i> Asset Details</h6><div class="row"><div class="col-md-4 mb-3"><label class="form-label text-muted-custom small">Geräte-Kategorie</label><select id="f_asset_type" class="form-select">${catOptions}</select></div><div class="col-md-4 mb-3"><label class="form-label text-muted-custom small">OS</label><input type="text" id="f_os" class="form-control" value="${isEdit && editItem.os ? editItem.os : ''}"></div><div class="col-md-4 mb-3"><label class="form-label text-muted-custom small">Hardware</label><input type="text" id="f_hardware" class="form-control" value="${isEdit && editItem.hardware ? editItem.hardware : ''}"></div></div><div class="row"><div class="col-md-6 mb-3"><label class="form-label text-muted-custom small">Seriennummer</label><input type="text" id="f_serial" class="form-control" value="${isEdit && editItem.serial_number ? editItem.serial_number : ''}"></div><div class="col-md-6 mb-3"><label class="form-label text-muted-custom small">MAC</label><input type="text" id="f_mac" class="form-control" value="${isEdit && editItem.mac_address ? editItem.mac_address : ''}"></div></div><div class="row"><div class="col-md-6 mb-3"><label class="form-label text-muted-custom small">Standort</label><input type="text" id="f_location" class="form-control" value="${isEdit && editItem.location ? editItem.location : ''}"></div><div class="col-md-6 mb-3"><label class="form-label text-muted-custom small">Garantie</label><input type="text" id="f_warranty" class="form-control" value="${isEdit && editItem.warranty ? editItem.warranty : ''}"></div></div></div><div class="d-flex gap-2 justify-content-end">${isEdit ? `<button class="btn btn-outline-secondary" onclick="window.showConnectionDetails(${JSON.stringify(editItem).replace(/"/g, '"')})">Abbrechen</button>` : ''}<button class="btn btn-primary px-4" onclick="window.saveConnection()"><i class="bi bi-floppy"></i> Speichern</button></div></div></div>`;
|
||||
};
|
||||
|
||||
window.showConnectionDetails = function(item) {
|
||||
if(window.isMapPage) return; const protoInfo = window.getProtoInfo(item.type); const children = window.connections.filter(c => c.parent_id == item.id); let tagsHtml = ''; if (item.tags) tagsHtml = item.tags.split(/[\s,]+/).filter(t=>t).map(t => `<span class="tag-badge">${t.startsWith('#')?t:'#'+t}</span>`).join('');
|
||||
let uplinkName = '-'; if(item.uplink_id) { const up = window.connections.find(c => c.id == item.uplink_id); let portInfo = ''; if(item.local_port && item.uplink_port) portInfo = `(${item.local_port} ➔ ${item.uplink_port})`; else if(item.local_port) portInfo = `(Lokaler Port: ${item.local_port})`; else if(item.uplink_port) portInfo = `(Zielport: ${item.uplink_port})`; if(up) uplinkName = `<span class="fw-bold text-primary" style="cursor:pointer;" onclick="window.showConnectionDetails(${JSON.stringify(up).replace(/"/g, '"')})">${up.name}</span> <span class="text-muted-custom small">${portInfo}</span>`; }
|
||||
let childRows = ''; if (children.length > 0) { childRows = `<div class="mt-4"><h6 class="text-muted-custom mb-3">Unterverbindungen (${children.length})</h6>`; children.forEach(c => { childRows += `<div class="dev-row mb-2 rounded border-bottom" style="cursor:pointer;" onclick="window.showConnectionDetails(${JSON.stringify(c).replace(/"/g, '"')})"><div class="d-flex align-items-center gap-3"><i class="bi ${window.getProtoInfo(c.type).icon} text-warning fs-3"></i><div><div class="fw-bold">${c.name}</div><div class="text-muted-custom small">${c.username || '-'}</div></div></div></div>`; }); childRows += `</div>`; }
|
||||
let adminActions = ''; if (window.currentUser.role === 'admin') { const isLocked = item.map_locked == 1; const lockIcon = isLocked ? 'bi-pin-angle-fill text-danger' : 'bi-pin-angle text-secondary'; const lockText = isLocked ? 'Fixierung lösen' : 'Auf Map fixieren'; adminActions = `<div class="dev-action-group"><div class="dev-action-title">Bearbeiten</div><div class="row"><div class="col-6 dev-action-btn" onclick="window.showConnectionForm()"><i class="bi bi-plus-lg text-success" style="font-size:1.2rem;"></i><span style="font-size:0.8rem;">Neu</span></div><div class="col-6 dev-action-btn" onclick="window.showConnectionForm(${JSON.stringify(item).replace(/"/g, '"')})"><i class="bi bi-pencil text-success" style="font-size:1.2rem;"></i><span style="font-size:0.8rem;">Edit</span></div></div><div class="dev-action-btn mt-2" onclick="window.toggleMapLock(${item.id}, ${isLocked ? 0 : 1})"><i class="bi ${lockIcon}" style="font-size:1.2rem;"></i><span style="font-size:0.8rem;">${lockText}</span></div><div class="dev-action-btn mt-2" onclick="window.deleteConnection(${item.id})"><i class="bi bi-trash text-danger" style="font-size:1.2rem;"></i><span style="font-size:0.8rem;">Löschen</span></div></div>`; }
|
||||
|
||||
document.getElementById('main-area').innerHTML = `<div class="dev-action-sidebar"><div class="dev-action-group"><div class="dev-action-title">Aktionen</div><div class="dev-action-btn" onclick="window.executeConnection('${item.type}', '${item.host}', '${item.port}', '${item.username}', '${item.password}')"><i class="bi bi-play-circle text-primary"></i><span>Sitzung öffnen</span></div></div>${item.type === 'web' ? `<div class="dev-action-group"><div class="dev-action-btn" onclick="window.open('${item.host}', '_blank')"><i class="bi bi-box-arrow-up-right text-primary"></i><span>Neuer Tab</span></div></div>` : ''}${adminActions}</div><div class="dev-main-view"><div class="dev-header"><i class="bi ${protoInfo.icon} text-primary" style="font-size: 2.5rem;"></i><div><h4 class="m-0 fw-bold">${item.name}</h4><div class="text-muted-custom small">${protoInfo.name}</div></div></div><div class="dev-tabs"><div class="dev-tab active" id="tab-btn-overview" onclick="window.switchTab('overview')">Übersicht</div><div class="dev-tab" id="tab-btn-asset" onclick="window.switchTab('asset')">Asset Info</div><div class="dev-tab" id="tab-btn-children" onclick="window.switchTab('children')">Unterverbindungen (${children.length})</div></div><div class="dev-content"><div id="view-overview" class="tab-view">${item.type === 'web' ? `<div class="mb-4" style="height: 400px;"><iframe src="${item.host}" style="width:100%; height:100%; border:1px solid var(--border-color); border-radius:4px;"></iframe></div>` : ''}<div class="rounded border mb-4" style="overflow: hidden;"><div class="dev-row"><div><div class="dev-row-label">Uplink (hängt an)</div><div class="dev-row-value">${uplinkName}</div></div></div><div class="dev-row"><div><div class="dev-row-label">Host</div><div class="dev-row-value">${item.host}${item.port ? ':'+item.port : ''}</div></div><div class="dev-row-tools"><i class="bi bi-copy" onclick="window.copyToClipboard('${item.host}${item.port ? ':'+item.port : ''}')"></i></div></div><div class="dev-row"><div><div class="dev-row-label">Benutzername</div><div class="dev-row-value">${item.username || '-'}</div></div><div class="dev-row-tools"><i class="bi bi-copy" onclick="window.copyToClipboard('${item.username}')"></i></div></div>
|
||||
|
||||
<div class="dev-row"><div><div class="dev-row-label">Passwort</div><div class="dev-row-value" id="pwd-val-${item.id}">••••••••</div></div><div class="dev-row-tools"><i class="bi bi-clock-history text-info" title="Passwort-Historie" onclick="window.showPasswordHistory(${item.id})"></i><i class="bi bi-eye" id="icon-pwd-val-${item.id}" onclick="window.togglePwd('pwd-val-${item.id}', '${item.password}')"></i><i class="bi bi-copy" onclick="window.copyToClipboard('${item.password}')"></i></div></div>
|
||||
|
||||
${tagsHtml ? `<div class="dev-row"><div><div class="dev-row-label">Tags</div><div class="dev-row-value mt-1">${tagsHtml}</div></div></div>` : ''}</div>${item.kuma_url ? `<div class="mb-4"><div class="dev-row-label fw-bold mb-2">Systemstatus (Uptime Kuma)</div>${item.kuma_url.includes('/badge/') ? `<img src="${item.kuma_url}" style="border-radius:4px; max-width:100%;">` : `<div style="height: 120px; border-radius: 4px; overflow: hidden; border: 1px solid var(--border-color);"><iframe src="${item.kuma_url}" style="width:100%; height:100%; border:none;"></iframe></div>`}</div>` : ''}</div><div id="view-asset" class="tab-view" style="display:none;"><div class="rounded border mb-4" style="overflow: hidden;"><div class="dev-row"><div><div class="dev-row-label">Kategorie</div><div class="dev-row-value"><span class="badge bg-secondary">${item.asset_type || '-'}</span></div></div></div><div class="dev-row"><div><div class="dev-row-label">Betriebssystem</div><div class="dev-row-value">${item.os || '-'}</div></div><div class="dev-row-tools"><i class="bi bi-copy" onclick="window.copyToClipboard('${item.os}')"></i></div></div><div class="dev-row"><div><div class="dev-row-label">Hardware / Modell</div><div class="dev-row-value">${item.hardware || '-'}</div></div><div class="dev-row-tools"><i class="bi bi-copy" onclick="window.copyToClipboard('${item.hardware}')"></i></div></div><div class="dev-row"><div><div class="dev-row-label">Seriennummer</div><div class="dev-row-value">${item.serial_number || '-'}</div></div><div class="dev-row-tools"><i class="bi bi-copy" onclick="window.copyToClipboard('${item.serial_number}')"></i></div></div><div class="dev-row"><div><div class="dev-row-label">MAC-Adresse</div><div class="dev-row-value" style="font-family: monospace;">${item.mac_address || '-'}</div></div><div class="dev-row-tools"><i class="bi bi-copy" onclick="window.copyToClipboard('${item.mac_address}')"></i></div></div><div class="dev-row"><div><div class="dev-row-label">Standort (Rack / HE)</div><div class="dev-row-value">${item.location || '-'}</div></div><div class="dev-row-tools"><i class="bi bi-copy" onclick="window.copyToClipboard('${item.location}')"></i></div></div><div class="dev-row"><div><div class="dev-row-label">Garantie-Ablauf</div><div class="dev-row-value">${item.warranty || '-'}</div></div></div></div></div><div id="view-children" class="tab-view" style="display:none;">${children.length > 0 ? childRows : '<div class="text-muted-custom">Keine Unterverbindungen vorhanden.</div>'}</div></div></div>`;
|
||||
};
|
||||
|
||||
window.saveConnection = async function() { if(window.currentUser.role !== 'admin') return; const t = document.getElementById('f_target').value; const data = { id: document.getElementById('f_conn_id').value || null, folder_name: t.startsWith('folder:') ? t.substring(7) : null, parent_id: t.startsWith('conn:') ? t.substring(5) : null, name: document.getElementById('f_conn_name').value, type: document.getElementById('f_type').value, host: document.getElementById('f_host').value, port: document.getElementById('f_port').value, username: document.getElementById('f_user').value, password: document.getElementById('f_pass').value, kuma_url: document.getElementById('f_kuma').value, tags: document.getElementById('f_tags').value, asset_type: document.getElementById('f_asset_type').value, os: document.getElementById('f_os').value, hardware: document.getElementById('f_hardware').value, serial_number: document.getElementById('f_serial').value, mac_address: document.getElementById('f_mac').value, location: document.getElementById('f_location').value, warranty: document.getElementById('f_warranty').value, uplink_id: document.getElementById('f_uplink_id').value || null, uplink_port: document.getElementById('f_uplink_port').value, local_port: document.getElementById('f_local_port').value }; await fetch('api.php?action=connections', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await window.loadData(); window.showHomeDashboard(); };
|
||||
window.executeConnection = function(type, host, port, user, pass) { if(type === 'web') return; const p = port || "-"; const u = user || "-"; const pwd = pass || "-"; window.location.href = `my${type}://${host};${p};${u};${pwd}`; };
|
||||
window.deleteConnection = async function(id) { if(window.currentUser.role !== 'admin') return; if(!confirm("Wirklich löschen?")) return; await fetch('api.php?action=delete_connection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({id: id}) }); await window.loadData(); window.showHomeDashboard(); };
|
||||
window.toggleMapLock = async function(id, lockState) { if(window.currentUser.role !== 'admin') return; await fetch('api.php?action=toggle_map_lock', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({id: id, locked: lockState}) }); await window.loadData(); const item = window.connections.find(c => c.id == id); if (item && !window.isMapPage) window.showConnectionDetails(item); };
|
||||
|
||||
window.loadMy2FA = async function() { const data = await (await fetch('api.php?action=my_2fa')).json(); const container = document.getElementById('2fa-container'); if (data.enabled) { container.innerHTML = `<div class="alert alert-success"><i class="bi bi-shield-check"></i> 2FA ist aktiv.</div><button class="btn btn-danger" onclick="window.disable2FA()">2FA deaktivieren</button>`; } else { container.innerHTML = `<div class="d-flex gap-4 align-items-center"><img src="${data.qr}" style="border-radius: 8px; border: 1px solid var(--border-color);"><div><p class="small text-muted-custom">Scanne diesen Code mit dem Google Authenticator.<br>Secret (Manuell): <code>${data.secret}</code></p><input type="text" id="setup_2fa_code" class="form-control mb-2" placeholder="6-stelliger Code"><button class="btn btn-success" onclick="window.enable2FA('${data.secret}')">Aktivieren</button></div></div>`; } };
|
||||
window.enable2FA = async function(secret) { const code = document.getElementById('setup_2fa_code').value; const res = await fetch('api.php?action=my_2fa', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({secret: secret, code: code}) }); if(res.ok) { window.showToast("2FA aktiviert!"); window.loadMy2FA(); } else { alert("Code falsch!"); } };
|
||||
window.disable2FA = async function() { if(!confirm("2FA wirklich deaktivieren?")) return; await fetch('api.php?action=my_2fa', { method: 'DELETE' }); window.showToast("2FA deaktiviert!"); window.loadMy2FA(); };
|
||||
|
||||
window.showSettings = function() {
|
||||
if(window.isMapPage) return; document.querySelectorAll('.connection-item').forEach(el => el.classList.remove('active')); const mainArea = document.getElementById('main-area'); let adminTabs = ''; let adminContent = '';
|
||||
|
||||
if (window.currentUser.role === 'admin') {
|
||||
let userRows = window.users.map(u => `<tr><td>${u.username}</td><td><span class="badge ${u.role==='admin'?'bg-danger':'bg-primary'}">${u.role}</span></td><td>${u.totp_enabled ? '<i class="bi bi-shield-check text-success"></i>' : '-'}</td><td class="text-end"><button class="btn btn-sm btn-outline-secondary me-1" onclick="window.editUser(${u.id}, '${u.username}', '${u.role}')"><i class="bi bi-pencil"></i></button><button class="btn btn-sm btn-outline-danger" onclick="window.deleteUser(${u.id})"><i class="bi bi-trash"></i></button></td></tr>`).join('');
|
||||
let protoRows = window.protocols.map(p => `<tr><td><i class="bi ${p.icon} text-primary"></i> ${p.name}</td><td><span class="badge bg-secondary">${p.proto_key}</span></td><td class="text-muted-custom small">${p.exec_path}</td><td class="text-end">${p.proto_key !== 'web' ? `<button class="btn btn-sm btn-outline-danger" onclick="window.deleteProtocol(${p.id})"><i class="bi bi-trash"></i></button>` : ''}</td></tr>`).join('');
|
||||
|
||||
let auditRows = '';
|
||||
if(window.auditLogs && window.auditLogs.length > 0) {
|
||||
auditRows = window.auditLogs.map(l => `<tr><td class="text-muted-custom small">${l.timestamp}</td><td class="fw-bold">${l.user}</td><td><span class="badge bg-secondary">${l.action}</span></td><td class="small">${l.details}</td></tr>`).join('');
|
||||
} else { auditRows = `<tr><td colspan="4" class="text-center text-muted-custom">Keine Einträge vorhanden.</td></tr>`; }
|
||||
|
||||
adminTabs = `<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-general">System & Daten</a></li><li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-users">Benutzer</a></li><li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-services">Dienste</a></li><li class="nav-item"><a class="nav-link text-danger" data-bs-toggle="tab" href="#tab-audit"><i class="bi bi-journal-code"></i> Audit-Log</a></li>`;
|
||||
|
||||
// NEU: Eingabefeld für ausgeschlossene Passwort-Zeichen im Tab "System & Daten"
|
||||
adminContent = `
|
||||
<div class="tab-pane fade" id="tab-general">
|
||||
<h5>Startseite / Dashboard</h5>
|
||||
<div class="row g-2 align-items-center mb-4"><div class="col-md-9"><input type="text" id="s_dashboard_url" class="form-control" value="${window.securityDashboardUrl}"></div><div class="col-md-3 d-grid"></div></div>
|
||||
|
||||
<h5>Passwort-Generator</h5>
|
||||
<div class="row g-2 align-items-center mb-4">
|
||||
<div class="col-md-9"><input type="text" id="s_pwd_exclude" class="form-control" placeholder="Ausgeschlossene Zeichen (z.B. lI1O0)" value="${window.appSettings.pwd_exclude_chars || ''}"></div>
|
||||
<div class="col-md-3 d-grid"><button class="btn btn-primary" onclick="window.saveGeneralSettings()"><i class="bi bi-floppy"></i> Speichern</button></div>
|
||||
</div>
|
||||
|
||||
<h5>Daten & Import</h5>
|
||||
<div class="d-flex gap-3 align-items-center"><button class="btn btn-outline-primary" onclick="window.exportCSV()"><i class="bi bi-download"></i> CSV Export</button><span class="text-muted-custom">|</span><div class="input-group" style="max-width: 400px;"><input type="file" class="form-control" id="csvFileInput" accept=".csv"><button class="btn btn-success" onclick="window.importCSV()"><i class="bi bi-upload"></i> Import</button></div></div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tab-users"><table class="table table-sm table-hover" style="color: var(--text-main);"><thead><tr><th>Benutzername</th><th>Rolle</th><th>2FA</th><th></th></tr></thead><tbody>${userRows}</tbody></table><div class="p-3 border rounded mt-3" style="border-color: var(--border-color)!important;"><h6>Benutzer anlegen/bearbeiten</h6><input type="hidden" id="u_id"><div class="row g-2 align-items-end"><div class="col-md-3"><input type="text" id="u_name" class="form-control form-control-sm" placeholder="Benutzername"></div><div class="col-md-3"><input type="password" id="u_pass" class="form-control form-control-sm" placeholder="Passwort (leer = nicht ändern)"></div><div class="col-md-3"><select id="u_role" class="form-select form-select-sm"><option value="viewer">Viewer</option><option value="admin">Admin</option></select></div><div class="col-md-3 d-grid"><button class="btn btn-primary btn-sm" onclick="window.saveUser()">Speichern</button></div></div></div></div>
|
||||
<div class="tab-pane fade" id="tab-services"><div class="d-flex justify-content-between mb-3"><h5>Dienste</h5><a href="api.php?action=generate_setup" class="btn btn-sm btn-success"><i class="bi bi-windows"></i> Setup laden</a></div><table class="table table-sm table-hover" style="color: var(--text-main);"><thead><tr><th>Dienst</th><th>Kürzel</th><th>Pfad</th><th></th></tr></thead><tbody>${protoRows}</tbody></table><div class="row g-2 mt-2 align-items-end p-3 border rounded" style="border-color: var(--border-color)!important;"><div class="col-md-3"><input type="text" id="p_key" class="form-control form-control-sm" placeholder="Kürzel (ssh)"></div><div class="col-md-3"><input type="text" id="p_name" class="form-control form-control-sm" placeholder="Name (PuTTY)"></div><div class="col-md-4"><input type="text" id="p_path" class="form-control form-control-sm" placeholder="Pfad (C:\\putty.exe)"></div><div class="col-md-2 d-grid"><button class="btn btn-primary btn-sm" onclick="window.addProtocol()">Neu</button></div></div></div>
|
||||
<div class="tab-pane fade" id="tab-audit"><div class="d-flex justify-content-between align-items-center mb-3"><h5>Audit-Log</h5><span class="text-muted-custom small">Zeigt die letzten 200 Aktionen</span></div><div class="table-responsive border rounded" style="max-height: 500px; overflow-y: auto; border-color: var(--border-color)!important;"><table class="table table-sm table-hover mb-0" style="color: var(--text-main); font-size: 0.9rem;"><thead style="position: sticky; top: 0; background: var(--bg-card); z-index: 1;"><tr><th>Zeitstempel</th><th>Benutzer</th><th>Aktion</th><th>Details</th></tr></thead><tbody>${auditRows}</tbody></table></div></div>
|
||||
`;
|
||||
} else { adminContent = `<div class="alert alert-info mt-3">Nur Administratoren können globale Einstellungen ändern. Lade dein lokales PC Setup-Skript <a href="api.php?action=generate_setup" class="alert-link">hier herunter</a>.</div>`; }
|
||||
mainArea.innerHTML = `<div class="w-100 h-100 overflow-auto p-4"><div class="dashboard-card mx-auto" style="max-width: 900px;"><h4 class="mb-4"><i class="bi bi-person-circle text-primary"></i> Profil & Einstellungen</h4><ul class="nav nav-tabs mb-4"><li class="nav-item"><a class="nav-link active" data-bs-toggle="tab" href="#tab-profile">Mein Profil</a></li>${adminTabs}</ul><div class="tab-content"><div class="tab-pane fade show active" id="tab-profile"><h5>Zwei-Faktor-Authentifizierung (2FA)</h5><p class="small text-muted-custom mb-4">Schütze deinen Account zusätzlich mit einem TOTP-Code.</p><div id="2fa-container">Lade 2FA Daten...</div></div>${adminContent}</div></div></div>`;
|
||||
window.loadMy2FA();
|
||||
};
|
||||
|
||||
window.saveUser = async function() { const data = { id: document.getElementById('u_id').value, username: document.getElementById('u_name').value, password: document.getElementById('u_pass').value, role: document.getElementById('u_role').value }; if(!data.username) return alert("Benutzername fehlt"); await fetch('api.php?action=users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await window.loadData(); window.showSettings(); };
|
||||
window.editUser = function(id, name, role) { document.getElementById('u_id').value = id; document.getElementById('u_name').value = name; document.getElementById('u_role').value = role; };
|
||||
window.deleteUser = async function(id) { if(!confirm("User löschen?")) return; await fetch('api.php?action=users', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({id: id}) }); await window.loadData(); window.showSettings(); };
|
||||
|
||||
window.saveGeneralSettings = async function() {
|
||||
const data = {
|
||||
dashboard_url: document.getElementById('s_dashboard_url').value,
|
||||
pwd_exclude_chars: document.getElementById('s_pwd_exclude').value
|
||||
};
|
||||
await fetch('api.php?action=settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
|
||||
await window.loadData();
|
||||
window.showToast("Einstellungen gespeichert!");
|
||||
};
|
||||
|
||||
window.addProtocol = async function() { const data = { proto_key: document.getElementById('p_key').value, name: document.getElementById('p_name').value, exec_path: document.getElementById('p_path').value, args: '', icon: 'bi-terminal' }; await fetch('api.php?action=protocols', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await window.loadData(); window.showSettings(); };
|
||||
window.deleteProtocol = async function(id) { await fetch('api.php?action=protocols', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({id: id}) }); await window.loadData(); window.showSettings(); };
|
||||
window.exportCSV = function() { const esc = (s) => '"' + String(s || '').replace(/"/g, '""') + '"'; let csv = "folder,name,type,host,port,username,password,kuma_url,tags,asset_type,os,hardware,serial_number,mac_address,location,warranty,uplink_id,uplink_port,local_port\n"; window.connections.forEach(c => { csv += [esc(c.folder_name), esc(c.name), esc(c.type), esc(c.host), esc(c.port), esc(c.username), esc(c.password), esc(c.kuma_url), esc(c.tags), esc(c.asset_type), esc(c.os), esc(c.hardware), esc(c.serial_number), esc(c.mac_address), esc(c.location), esc(c.warranty), esc(c.uplink_id), esc(c.uplink_port), esc(c.local_port)].join(',') + "\n"; }); const a = document.createElement('a'); a.href = window.URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8;' })); a.download = 'rdm_export.csv'; a.click(); };
|
||||
window.importCSV = async function() { const f = document.getElementById('csvFileInput').files[0]; if (!f) return alert("Datei wählen."); const reader = new FileReader(); reader.onload = async (e) => { const lines = e.target.result.split(/\r?\n/).filter(line => line.trim() !== ''); const delimiter = lines[0].includes(';') ? ';' : ','; const headers = lines[0].split(delimiter).map(h => h.trim().toLowerCase().replace(/^"|"$/g, '')); const data = []; for (let i = 1; i < lines.length; i++) { const regex = new RegExp(`(\\${delimiter}|\\r?\\n|\\r|^)(?:"([^"]*(?:""[^"]*)*)"|([^"\\${delimiter}\\r\\n]*))`, 'gi'); let values = []; let match; while (match = regex.exec(lines[i])) { values.push(match[2] ? match[2].replace(/""/g, '"') : match[3]); } let rowObj = {}; headers.forEach((h, idx) => rowObj[h] = values[idx] || ''); if(rowObj['name'] && rowObj['host']) data.push(rowObj); } if(data.length === 0) return alert("Keine gültigen Zeilen."); const res = await (await fetch('api.php?action=import_connections', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) })).json(); if (res.status === "success") { window.showToast("Import erfolgreich!"); await window.loadData(); window.showSettings(); } }; reader.readAsText(f); };
|
||||
window.exportMapPNG = function() { const canvas = document.querySelector('#mynetwork canvas'); if(!canvas) { alert("Fehler: Karte konnte nicht im Browser gefunden werden."); return; } const dataUrl = canvas.toDataURL("image/png"); const a = document.createElement('a'); a.href = dataUrl; a.download = 'Netzwerk-Topologie.png'; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.showToast("Export erfolgreich!"); };
|
||||
window.toggleFullScreen = function() { const elem = document.getElementById('mynetwork').parentElement; if (!document.fullscreenElement) { elem.requestFullscreen().catch(err => { alert("Fullscreen API wird nicht unterstützt."); }); } else { document.exitFullscreen(); } };
|
||||
window.filterMap = function(category) { if (!window.networkInstance) return; const allNodes = window.networkInstance.body.data.nodes.get(); const updates = []; allNodes.forEach(n => { if (n.id.startsWith('folder_')) return; const connId = n.id.replace('conn_', ''); const conn = window.connections.find(c => c.id == connId); let isMatch = !category || (conn && conn.asset_type === category); updates.push({ id: n.id, opacity: isMatch ? 1 : 0.15 }); }); window.networkInstance.body.data.nodes.update(updates); };
|
||||
window.resetMapPhysics = async function() { if(window.currentUser.role !== 'admin') return; if(!confirm("⚠️ ACHTUNG: Möchtest du wirklich alle fixierten Koordinaten löschen und die Schwerkraft neu berechnen lassen?")) return; await fetch('api.php?action=reset_map', { method: 'POST' }); window.showToast("Schwerkraft reaktiviert!"); await window.loadData(); };
|
||||
|
||||
window.drawMap = function() {
|
||||
if (!window.isMapPage) return;
|
||||
let containerWrapper = document.getElementById('mynetwork').parentElement;
|
||||
if(!document.getElementById('map-controls') && containerWrapper) {
|
||||
containerWrapper.style.position = 'relative'; const controls = document.createElement('div'); controls.id = 'map-controls';
|
||||
controls.style.cssText = "position:absolute; top:20px; right:20px; z-index:1000; background:var(--bg-card); padding:10px; border-radius:8px; border:1px solid var(--border-color); box-shadow:0 4px 10px rgba(0,0,0,0.3); display:flex; gap:10px; align-items:center;";
|
||||
let filterOptions = `<option value="">-- Alle Geräte --</option>`; window.categories.forEach(c => filterOptions += `<option value="${c.name}">${c.name}</option>`);
|
||||
controls.innerHTML = `<div class="d-flex gap-2 flex-wrap align-items-center"><select id="map-label-select" class="form-select form-select-sm w-auto" style="cursor:pointer;" onchange="window.mapLabelMode=this.value; window.drawMap()"><option value="name" ${window.mapLabelMode==='name'?'selected':''}>Nur Name</option><option value="ip" ${window.mapLabelMode==='ip'?'selected':''}>Nur IP</option><option value="both" ${window.mapLabelMode==='both'?'selected':''}>Name + IP</option></select><select id="map-filter-select" class="form-select form-select-sm w-auto" style="cursor:pointer;" onchange="window.filterMap(this.value)">${filterOptions}</select><button class="btn btn-sm btn-outline-secondary" onclick="window.networkInstance.fit()" title="Ansicht zentrieren"><i class="bi bi-arrows-collapse"></i></button><button class="btn btn-sm btn-outline-secondary" onclick="window.toggleFullScreen()" title="Vollbild Modus"><i class="bi bi-arrows-fullscreen"></i></button><button class="btn btn-sm btn-danger" onclick="window.resetMapPhysics()" title="Chaos Modus (Alles zurücksetzen)"><i class="bi bi-tornado"></i></button><button class="btn btn-sm btn-success fw-bold" onclick="window.exportMapPNG()"><i class="bi bi-camera"></i> PNG</button></div>`;
|
||||
containerWrapper.appendChild(controls);
|
||||
}
|
||||
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; const isDark = currentTheme === 'dark'; const textColor = isDark ? '#f1f1f1' : '#333333'; const folderColor = isDark ? '#333333' : '#e2e8f0'; const highlightEdgeColor = isDark ? '#3f98e7' : '#0078d7';
|
||||
const nodes = new vis.DataSet(); const edges = new vis.DataSet(); const folderSet = new Set();
|
||||
const groupColors = { 'rdp': { background: '#0078d7', border: '#005a9e' }, 'ssh': { background: '#2b2b2b', border: '#000000' }, 'web': { background: '#107c41', border: '#0b592f' }, 'default': { background: '#6c757d', border: '#495057' } };
|
||||
|
||||
window.connections.forEach(c => {
|
||||
if (c.folder_name && !c.uplink_id && !folderSet.has(c.folder_name)) { folderSet.add(c.folder_name); const shortName = c.folder_name.split('/').pop(); nodes.add({ id: 'folder_' + c.folder_name, label: shortName, shape: 'box', color: { background: folderColor, border: '#aaaaaa' }, font: { color: textColor, size: 16, bold: true }, margin: 10 }); }
|
||||
let tooltip = `<strong>${c.name}</strong><br>${c.host}${c.port ? ':'+c.port : ''}<br>`; if(c.asset_type) tooltip += `<span style="color:var(--primary-color)">Typ:</span> ${c.asset_type}<br>`; if(c.hardware) tooltip += `<span style="color:var(--primary-color)">Hardware:</span> ${c.hardware}<br>`; if(c.local_port && c.uplink_port) tooltip += `<span style="color:var(--primary-color)">Ports:</span> ${c.local_port} ➔ ${c.uplink_port}`;
|
||||
let nodeShape = c.uplink_id ? 'box' : 'dot'; let nodeImage = undefined; let nodeFont = { color: c.uplink_id ? '#fff' : textColor, size: c.parent_id ? 12 : 14 };
|
||||
let labelText = ''; let typeBrackets = c.asset_type ? '\n[' + c.asset_type + ']' : '';
|
||||
if (window.mapLabelMode === 'name') { labelText = c.name + typeBrackets; } else if (window.mapLabelMode === 'ip') { labelText = (c.host || 'Keine IP') + typeBrackets; } else if (window.mapLabelMode === 'both') { labelText = c.name + '\n' + (c.host || 'Keine IP') + typeBrackets; }
|
||||
if (c.asset_type) { const catInfo = window.categories.find(cat => cat.name === c.asset_type); if (catInfo) { nodeShape = 'image'; nodeImage = catInfo.url; nodeFont.color = textColor; nodeFont.background = isDark ? 'rgba(30,30,30,0.7)' : 'rgba(255,255,255,0.7)'; } }
|
||||
let nodeOptions = { id: 'conn_' + c.id, label: c.map_locked == 1 ? "📌 " + labelText : labelText, shape: nodeShape, image: nodeImage, size: nodeImage ? 25 : (c.parent_id ? 10 : 18), color: groupColors[c.type] || groupColors['default'], font: nodeFont, title: tooltip };
|
||||
if (c.map_x !== null && c.map_y !== null) { nodeOptions.x = parseFloat(c.map_x); nodeOptions.y = parseFloat(c.map_y); nodeOptions.physics = false; }
|
||||
if (c.map_locked == 1) nodeOptions.fixed = true;
|
||||
const filterVal = document.getElementById('map-filter-select') ? document.getElementById('map-filter-select').value : ''; if(filterVal && c.asset_type !== filterVal) { nodeOptions.opacity = 0.15; }
|
||||
nodes.add(nodeOptions);
|
||||
if (c.uplink_id) { let edgeLabel = ''; if (c.local_port && c.uplink_port) edgeLabel = `${c.local_port} ➔ ${c.uplink_port}`; else if (c.local_port) edgeLabel = `${c.local_port} ➔`; else if (c.uplink_port) edgeLabel = `➔ ${c.uplink_port}`; edges.add({ from: 'conn_' + c.uplink_id, to: 'conn_' + c.id, label: edgeLabel, arrows: { to: { enabled: true, scaleFactor: 0.6 } }, font: { align: 'middle', size: 11, color: textColor, background: isDark ? '#1e1e1e' : '#ffffff', strokeWidth: 0 }, color: { color: highlightEdgeColor, highlight: highlightEdgeColor }, width: 2, length: 180 }); } else if (c.parent_id) { edges.add({ from: 'conn_' + c.parent_id, to: 'conn_' + c.id, dashes: true, color: { color: '#aaaaaa' }, length: 80 }); } else if (c.folder_name) { edges.add({ from: 'folder_' + c.folder_name, to: 'conn_' + c.id, color: { color: '#aaaaaa', opacity: 0.4 }, length: 250 }); }
|
||||
});
|
||||
const container = document.getElementById('mynetwork'); const data = { nodes: nodes, edges: edges }; const options = { physics: { stabilization: true, barnesHut: { gravitationalConstant: -2500, springLength: 180, springConstant: 0.04 } }, edges: { smooth: false }, interaction: { hover: true, zoomView: true, dragView: true } };
|
||||
window.networkInstance = new vis.Network(container, data, options);
|
||||
window.networkInstance.on("dragEnd", async function (params) { if (window.currentUser.role !== 'admin') return; if (params.nodes.length > 0) { const nodeId = params.nodes[0]; if(nodeId.startsWith('conn_')) { const id = nodeId.replace('conn_', ''); const pos = window.networkInstance.getPositions([nodeId])[nodeId]; await fetch('api.php?action=save_map_pos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({id: id, x: pos.x, y: pos.y}) }); const conn = window.connections.find(c => c.id == id); if(conn) { conn.map_x = pos.x; conn.map_y = pos.y; } } } });
|
||||
};
|
||||
|
||||
window.spawnAsteroidsShip = function() {
|
||||
if(!window.isMapPage || document.getElementById('asteroids-ship')) return; const mapContainer = document.getElementById('mynetwork'); if(!mapContainer) return;
|
||||
const ship = document.createElement('div'); ship.id = 'asteroids-ship'; ship.innerHTML = `<svg viewBox="0 0 20 20" width="30" height="30" style="filter: drop-shadow(0 0 5px #00ff00);"><polygon points="10,0 20,20 10,15 0,20" fill="none" stroke="#00ff00" stroke-width="1.5"/></svg>`;
|
||||
const startY = Math.random() * mapContainer.offsetHeight; ship.style.cssText = `position: absolute; top: ${startY}px; left: -50px; z-index: 10; pointer-events: none; transition: transform 10s linear; transform: rotate(90deg);`;
|
||||
mapContainer.appendChild(ship);
|
||||
setTimeout(() => { const endY = Math.random() * mapContainer.offsetHeight; const endX = mapContainer.offsetWidth + 50; const angle = Math.atan2(endY - startY, endX - (-50)) * 180 / Math.PI; ship.style.transform = `translate(${endX}px, ${endY - startY}px) rotate(${angle + 90}deg)`; }, 100);
|
||||
setTimeout(() => { if(ship.parentNode) ship.parentNode.removeChild(ship); }, 10500);
|
||||
};
|
||||
setInterval(() => { if(Math.random() > 0.8) window.spawnAsteroidsShip(); }, 30000);
|
||||
|
||||
window.initTheme(); window.checkSession();
|
||||
Reference in New Issue
Block a user