Initial commit
This commit is contained in:
298
WebRDM/api.php
Normal file
298
WebRDM/api.php
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
session_start();
|
||||||
|
header("Content-Type: application/json");
|
||||||
|
|
||||||
|
$db_dir = __DIR__ . '/db';
|
||||||
|
if (!is_dir($db_dir)) { mkdir($db_dir, 0755, true); }
|
||||||
|
$db_file = $db_dir . '/rdm.db';
|
||||||
|
|
||||||
|
// --- KRYPTOGRAPHIE & SCHLÜSSEL ---
|
||||||
|
$key_file = $db_dir . '/secret.key';
|
||||||
|
if (!file_exists($key_file)) { file_put_contents($key_file, random_bytes(32)); }
|
||||||
|
$crypto_key = file_get_contents($key_file);
|
||||||
|
|
||||||
|
function encryptData($data, $key) {
|
||||||
|
if (!$data) return '';
|
||||||
|
if (strpos($data, 'ENC:') === 0) return $data;
|
||||||
|
$iv = random_bytes(16);
|
||||||
|
$cipher = openssl_encrypt($data, 'aes-256-cbc', $key, 0, $iv);
|
||||||
|
return 'ENC:' . base64_encode($iv . $cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decryptData($data, $key) {
|
||||||
|
if (!$data || strpos($data, 'ENC:') !== 0) return $data;
|
||||||
|
$raw = base64_decode(substr($data, 4));
|
||||||
|
$iv = substr($raw, 0, 16);
|
||||||
|
$cipher = substr($raw, 16);
|
||||||
|
return openssl_decrypt($cipher, 'aes-256-cbc', $key, 0, $iv);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logAudit($pdo, $user, $action, $details) {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO audit_logs (user, action, details) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$user, $action, $details]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTOTPSecret() { $b32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; $s = ""; for ($i = 0; $i < 16; $i++) $s .= $b32[random_int(0, 31)]; return $s; }
|
||||||
|
function verifyTOTP($secret, $code) {
|
||||||
|
if (empty($secret) || strlen($code) != 6) return false;
|
||||||
|
$b32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; $secret = strtoupper($secret); $dec = "";
|
||||||
|
for ($i = 0; $i < strlen($secret); $i++) { $n = strpos($b32, $secret[$i]); if ($n === false) return false; $dec .= str_pad(base_convert($n, 10, 2), 5, '0', STR_PAD_LEFT); }
|
||||||
|
$dec = str_split($dec, 8); $bin = ""; foreach ($dec as $b) $bin .= chr(bindec($b));
|
||||||
|
$time = floor(time() / 30); $timeBin = str_pad(pack('N', $time), 8, "\x00", STR_PAD_LEFT);
|
||||||
|
$hmac = hash_hmac('sha1', $timeBin, $bin, true); $offset = ord(substr($hmac, -1)) & 0x0F;
|
||||||
|
$value = unpack('N', substr($hmac, $offset, 4))[1] & 0x7FFFFFFF;
|
||||||
|
$calcCode = str_pad($value % 1000000, 6, '0', STR_PAD_LEFT); return hash_equals($calcCode, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("sqlite:" . $db_file);
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password TEXT, role TEXT, totp_secret TEXT, totp_enabled INTEGER DEFAULT 0)");
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS folders (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE)");
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS connections (id INTEGER PRIMARY KEY AUTOINCREMENT, folder_name TEXT, parent_id INTEGER DEFAULT NULL, name TEXT, type TEXT, host TEXT, port TEXT, username TEXT, password TEXT, kuma_url TEXT, tags TEXT, asset_type TEXT, os TEXT, hardware TEXT, serial_number TEXT, mac_address TEXT, location TEXT, warranty TEXT, uplink_id INTEGER, uplink_port TEXT, local_port TEXT, map_x REAL, map_y REAL, map_locked INTEGER DEFAULT 0)");
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS protocols (id INTEGER PRIMARY KEY AUTOINCREMENT, proto_key TEXT UNIQUE, name TEXT, exec_path TEXT, args TEXT, icon TEXT)");
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS templates (id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT, name TEXT, type TEXT, host TEXT, port TEXT, username TEXT, password TEXT, kuma_url TEXT, tags TEXT)");
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS settings (id INTEGER PRIMARY KEY AUTOINCREMENT, key_name TEXT UNIQUE, value TEXT)");
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS scripts (id INTEGER PRIMARY KEY AUTOINCREMENT, folder_name TEXT, name TEXT, language TEXT, script_body TEXT)");
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS audit_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, user TEXT, action TEXT, details TEXT)");
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS password_history (id INTEGER PRIMARY KEY AUTOINCREMENT, connection_id INTEGER, password TEXT, changed_at DATETIME DEFAULT CURRENT_TIMESTAMP)");
|
||||||
|
|
||||||
|
try { $pdo->exec("ALTER TABLE connections ADD COLUMN local_port TEXT"); } catch (Exception $e) {}
|
||||||
|
try { $pdo->exec("ALTER TABLE connections ADD COLUMN map_x REAL"); } catch (Exception $e) {}
|
||||||
|
try { $pdo->exec("ALTER TABLE connections ADD COLUMN map_y REAL"); } catch (Exception $e) {}
|
||||||
|
try { $pdo->exec("ALTER TABLE connections ADD COLUMN map_locked INTEGER DEFAULT 0"); } catch (Exception $e) {}
|
||||||
|
|
||||||
|
if ($pdo->query("SELECT COUNT(*) FROM users")->fetchColumn() == 0) {
|
||||||
|
$pdo->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, ?)")->execute(['admin', password_hash('admin', PASSWORD_DEFAULT), 'admin']);
|
||||||
|
logAudit($pdo, 'System', 'INSTALL', 'Admin user created.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pdo->query("SELECT COUNT(*) FROM protocols")->fetchColumn() == 0) {
|
||||||
|
$pdo->exec("INSERT INTO protocols (proto_key, name, exec_path, args, icon) VALUES ('rdp', 'RDP (Windows)', 'mstsc.exe', '/v:[HOST]', 'bi-windows'), ('ssh', 'SSH', 'putty.exe', '-ssh [USER]@[HOST]', 'bi-terminal'), ('web', 'Website', '', '', 'bi-globe')");
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->exec("INSERT OR IGNORE INTO settings (key_name, value) VALUES ('dashboard_url', 'https://threatmap.fortiguard.com/')");
|
||||||
|
$pdo->exec("INSERT OR IGNORE INTO settings (key_name, value) VALUES ('pwd_exclude_chars', 'lI1O0')");
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? ''; $method = $_SERVER['REQUEST_METHOD']; $input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if ($action === 'login' && $method === 'POST') {
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?"); $stmt->execute([$input['username']]); $user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($user && password_verify($input['password'], $user['password'])) {
|
||||||
|
if ($user['totp_enabled']) {
|
||||||
|
$_SESSION['pending_2fa'] = $user['id']; echo json_encode(["status" => "2fa_required"]);
|
||||||
|
} else {
|
||||||
|
$_SESSION['user_id'] = $user['id']; $_SESSION['role'] = $user['role']; $_SESSION['username'] = $user['username'];
|
||||||
|
logAudit($pdo, $user['username'], 'LOGIN', 'Erfolgreicher Login.');
|
||||||
|
echo json_encode(["status" => "success", "role" => $user['role'], "username" => $user['username']]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logAudit($pdo, $input['username'] ?? 'Unknown', 'LOGIN_FAILED', 'Falsche Zugangsdaten.');
|
||||||
|
http_response_code(401); echo json_encode(["error" => "Falsche Zugangsdaten"]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
elseif ($action === 'verify_2fa' && $method === 'POST') {
|
||||||
|
if (!isset($_SESSION['pending_2fa'])) { http_response_code(401); die(json_encode(["error" => "Session invalid"])); }
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?"); $stmt->execute([$_SESSION['pending_2fa']]); $user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (verifyTOTP($user['totp_secret'], $input['code'])) {
|
||||||
|
unset($_SESSION['pending_2fa']);
|
||||||
|
$_SESSION['user_id'] = $user['id']; $_SESSION['role'] = $user['role']; $_SESSION['username'] = $user['username'];
|
||||||
|
logAudit($pdo, $user['username'], 'LOGIN_2FA', 'Erfolgreicher 2FA Login.');
|
||||||
|
echo json_encode(["status" => "success", "role" => $user['role'], "username" => $user['username']]);
|
||||||
|
} else {
|
||||||
|
logAudit($pdo, $user['username'], 'LOGIN_2FA_FAILED', 'Falscher 2FA Code.');
|
||||||
|
http_response_code(401); echo json_encode(["error" => "Falscher 2FA Code"]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
elseif ($action === 'logout') {
|
||||||
|
if(isset($_SESSION['username'])) logAudit($pdo, $_SESSION['username'], 'LOGOUT', 'Benutzer abgemeldet.');
|
||||||
|
session_destroy(); echo json_encode(["status" => "success"]); exit;
|
||||||
|
}
|
||||||
|
elseif ($action === 'session') {
|
||||||
|
if(isset($_SESSION['user_id'])) echo json_encode(["status" => "active", "role" => $_SESSION['role'], "username" => $_SESSION['username']]);
|
||||||
|
else echo json_encode(["status" => "inactive"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) { http_response_code(401); die(json_encode(["error" => "Nicht eingeloggt"])); }
|
||||||
|
$isAdmin = ($_SESSION['role'] === 'admin');
|
||||||
|
$currentUser = $_SESSION['username'];
|
||||||
|
|
||||||
|
if ($action === 'categories' && $method === 'GET') {
|
||||||
|
$img_dir = __DIR__ . '/img/DeviceCategories'; if (!is_dir($img_dir)) { @mkdir($img_dir, 0755, true); }
|
||||||
|
$categories = [];
|
||||||
|
if (is_dir($img_dir)) {
|
||||||
|
$files = scandir($img_dir);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if ($file !== '.' && $file !== '..' && in_array(strtolower(pathinfo($file, PATHINFO_EXTENSION)), ['png', 'jpg', 'jpeg', 'gif', 'svg'])) {
|
||||||
|
$categories[] = ['name' => pathinfo($file, PATHINFO_FILENAME), 'url' => 'img/DeviceCategories/' . $file];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo json_encode($categories); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'users') {
|
||||||
|
if ($method === 'GET') {
|
||||||
|
if (!$isAdmin) die(json_encode(["error" => "Access denied"]));
|
||||||
|
echo json_encode($pdo->query("SELECT id, username, role, totp_enabled FROM users")->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
} elseif ($method === 'POST') {
|
||||||
|
if (!$isAdmin) die();
|
||||||
|
if (!empty($input['id'])) {
|
||||||
|
if(!empty($input['password'])) $pdo->prepare("UPDATE users SET username=?, role=?, password=? WHERE id=?")->execute([$input['username'], $input['role'], password_hash($input['password'], PASSWORD_DEFAULT), $input['id']]);
|
||||||
|
else $pdo->prepare("UPDATE users SET username=?, role=? WHERE id=?")->execute([$input['username'], $input['role'], $input['id']]);
|
||||||
|
logAudit($pdo, $currentUser, 'USER_UPDATE', "Benutzer {$input['username']} aktualisiert.");
|
||||||
|
} else {
|
||||||
|
$pdo->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, ?)")->execute([$input['username'], password_hash($input['password'], PASSWORD_DEFAULT), $input['role']]);
|
||||||
|
logAudit($pdo, $currentUser, 'USER_CREATE', "Benutzer {$input['username']} erstellt.");
|
||||||
|
}
|
||||||
|
echo json_encode(["status" => "success"]);
|
||||||
|
} elseif ($method === 'DELETE') {
|
||||||
|
if (!$isAdmin || $input['id'] == $_SESSION['user_id']) die();
|
||||||
|
$pdo->prepare("DELETE FROM users WHERE id = ?")->execute([$input['id']]);
|
||||||
|
logAudit($pdo, $currentUser, 'USER_DELETE', "Benutzer ID {$input['id']} gelöscht.");
|
||||||
|
echo json_encode(["status" => "success"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($action === 'audit' && $method === 'GET' && $isAdmin) {
|
||||||
|
echo json_encode($pdo->query("SELECT * FROM audit_logs ORDER BY timestamp DESC LIMIT 200")->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
}
|
||||||
|
elseif ($action === 'password_history' && $method === 'GET') {
|
||||||
|
$stmt = $pdo->prepare("SELECT password, changed_at FROM password_history WHERE connection_id = ? ORDER BY changed_at DESC");
|
||||||
|
$stmt->execute([$_GET['id']]);
|
||||||
|
$hist = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach($hist as &$h) { $h['password'] = decryptData($h['password'], $crypto_key); }
|
||||||
|
echo json_encode($hist);
|
||||||
|
}
|
||||||
|
elseif ($action === 'my_2fa') {
|
||||||
|
if ($method === 'GET') {
|
||||||
|
$stmt = $pdo->prepare("SELECT totp_secret, totp_enabled FROM users WHERE id = ?"); $stmt->execute([$_SESSION['user_id']]); $u = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$secret = $u['totp_secret'];
|
||||||
|
if (!$secret) { $secret = generateTOTPSecret(); $pdo->prepare("UPDATE users SET totp_secret = ? WHERE id = ?")->execute([$secret, $_SESSION['user_id']]); }
|
||||||
|
$url = "otpauth://totp/WebRDM:" . urlencode($_SESSION['username']) . "?secret=" . $secret . "&issuer=WebRDM";
|
||||||
|
echo json_encode(["secret" => $secret, "enabled" => $u['totp_enabled'], "qr" => "https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=" . urlencode($url)]);
|
||||||
|
} elseif ($method === 'POST') {
|
||||||
|
if (verifyTOTP($input['secret'], $input['code'])) {
|
||||||
|
$pdo->prepare("UPDATE users SET totp_enabled = 1 WHERE id = ?")->execute([$_SESSION['user_id']]);
|
||||||
|
logAudit($pdo, $currentUser, '2FA_ENABLED', '2FA aktiviert.');
|
||||||
|
echo json_encode(["status" => "success"]);
|
||||||
|
} else { http_response_code(400); echo json_encode(["error" => "Code inkorrekt"]); }
|
||||||
|
} elseif ($method === 'DELETE') {
|
||||||
|
$pdo->prepare("UPDATE users SET totp_enabled = 0, totp_secret = NULL WHERE id = ?")->execute([$_SESSION['user_id']]);
|
||||||
|
logAudit($pdo, $currentUser, '2FA_DISABLED', '2FA deaktiviert.');
|
||||||
|
echo json_encode(["status" => "success"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($action === 'folders') {
|
||||||
|
if ($method === 'GET') echo json_encode($pdo->query("SELECT name FROM folders ORDER BY name")->fetchAll(PDO::FETCH_COLUMN));
|
||||||
|
elseif ($method === 'POST' && $isAdmin) {
|
||||||
|
$pdo->prepare("INSERT OR IGNORE INTO folders (name) VALUES (:name)")->execute(['name' => $input['name']]);
|
||||||
|
logAudit($pdo, $currentUser, 'FOLDER_CREATE', "Ordner '{$input['name']}' erstellt.");
|
||||||
|
echo json_encode(["status" => "success"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($action === 'connections') {
|
||||||
|
if ($method === 'GET') {
|
||||||
|
$conns = $pdo->query("SELECT * FROM connections ORDER BY folder_name, name")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach($conns as &$c) { $c['password'] = decryptData($c['password'], $crypto_key); }
|
||||||
|
echo json_encode($conns);
|
||||||
|
}
|
||||||
|
elseif ($method === 'POST' && $isAdmin) {
|
||||||
|
$raw_pw = $input['password'] ?? '';
|
||||||
|
$old_pw_enc = '';
|
||||||
|
|
||||||
|
if (!empty($input['id'])) {
|
||||||
|
$stmt = $pdo->prepare("SELECT password FROM connections WHERE id = ?"); $stmt->execute([$input['id']]);
|
||||||
|
$old_conn = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$old_pw_enc = $old_conn['password'] ?? '';
|
||||||
|
$old_pw_decrypted = decryptData($old_pw_enc, $crypto_key);
|
||||||
|
|
||||||
|
if ($old_pw_decrypted !== '' && $old_pw_decrypted !== $raw_pw) {
|
||||||
|
$pdo->prepare("INSERT INTO password_history (connection_id, password) VALUES (?, ?)")->execute([$input['id'], $old_pw_enc]);
|
||||||
|
$pdo->prepare("DELETE FROM password_history WHERE id NOT IN (SELECT id FROM password_history WHERE connection_id = ? ORDER BY changed_at DESC LIMIT 10)")->execute([$input['id']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($input['password'])) $input['password'] = encryptData($input['password'], $crypto_key);
|
||||||
|
|
||||||
|
if (!empty($input['id'])) {
|
||||||
|
$pdo->prepare("UPDATE connections SET folder_name=:folder_name, parent_id=:parent_id, name=:name, type=:type, host=:host, port=:port, username=:username, password=:password, kuma_url=:kuma_url, tags=:tags, asset_type=:asset_type, os=:os, hardware=:hardware, serial_number=:serial_number, mac_address=:mac_address, location=:location, warranty=:warranty, uplink_id=:uplink_id, uplink_port=:uplink_port, local_port=:local_port WHERE id=:id")->execute($input);
|
||||||
|
logAudit($pdo, $currentUser, 'ASSET_UPDATE', "Verbindung '{$input['name']}' aktualisiert.");
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO connections (folder_name, parent_id, 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) VALUES (:folder_name, :parent_id, :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)");
|
||||||
|
unset($input['id']); $stmt->execute($input);
|
||||||
|
logAudit($pdo, $currentUser, 'ASSET_CREATE', "Verbindung '{$input['name']}' erstellt.");
|
||||||
|
}
|
||||||
|
echo json_encode(["status" => "success"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($action === 'delete_connection' && $method === 'POST' && $isAdmin) {
|
||||||
|
$pdo->prepare("DELETE FROM connections WHERE id = :id")->execute(['id' => $input['id']]);
|
||||||
|
$pdo->prepare("DELETE FROM password_history WHERE connection_id = :id")->execute(['id' => $input['id']]);
|
||||||
|
logAudit($pdo, $currentUser, 'ASSET_DELETE', "Verbindung ID {$input['id']} gelöscht.");
|
||||||
|
echo json_encode(["status" => "success"]);
|
||||||
|
}
|
||||||
|
elseif ($action === 'scripts') {
|
||||||
|
if ($method === 'GET') { echo json_encode($pdo->query("SELECT * FROM scripts ORDER BY folder_name, name")->fetchAll(PDO::FETCH_ASSOC)); }
|
||||||
|
elseif ($method === 'POST' && $isAdmin) {
|
||||||
|
$data = ['folder_name' => $input['folder_name'] ?: null, 'name' => $input['name'], 'language' => $input['language'], 'script_body' => $input['script_body']];
|
||||||
|
if (!empty($input['id'])) {
|
||||||
|
$data['id'] = $input['id'];
|
||||||
|
$pdo->prepare("UPDATE scripts SET folder_name=:folder_name, name=:name, language=:language, script_body=:script_body WHERE id=:id")->execute($data);
|
||||||
|
logAudit($pdo, $currentUser, 'SCRIPT_UPDATE', "Skript '{$input['name']}' aktualisiert.");
|
||||||
|
} else {
|
||||||
|
$pdo->prepare("INSERT INTO scripts (folder_name, name, language, script_body) VALUES (:folder_name, :name, :language, :script_body)")->execute($data);
|
||||||
|
logAudit($pdo, $currentUser, 'SCRIPT_CREATE', "Skript '{$input['name']}' erstellt.");
|
||||||
|
}
|
||||||
|
echo json_encode(["status" => "success"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($action === 'delete_script' && $method === 'POST' && $isAdmin) {
|
||||||
|
$pdo->prepare("DELETE FROM scripts WHERE id = :id")->execute(['id' => $input['id']]);
|
||||||
|
logAudit($pdo, $currentUser, 'SCRIPT_DELETE', "Skript ID {$input['id']} gelöscht.");
|
||||||
|
echo json_encode(["status" => "success"]);
|
||||||
|
}
|
||||||
|
elseif ($action === 'save_map_pos' && $method === 'POST' && $isAdmin) {
|
||||||
|
$pdo->prepare("UPDATE connections SET map_x = ?, map_y = ? WHERE id = ?")->execute([$input['x'], $input['y'], $input['id']]); echo json_encode(["status" => "success"]);
|
||||||
|
}
|
||||||
|
elseif ($action === 'toggle_map_lock' && $method === 'POST' && $isAdmin) {
|
||||||
|
$pdo->prepare("UPDATE connections SET map_locked = ? WHERE id = ?")->execute([$input['locked'], $input['id']]); echo json_encode(["status" => "success"]);
|
||||||
|
}
|
||||||
|
elseif ($action === 'reset_map' && $method === 'POST' && $isAdmin) {
|
||||||
|
$pdo->exec("UPDATE connections SET map_x = NULL, map_y = NULL, map_locked = 0");
|
||||||
|
logAudit($pdo, $currentUser, 'MAP_RESET', "Kartenphysik und Layout zurückgesetzt.");
|
||||||
|
echo json_encode(["status" => "success"]);
|
||||||
|
}
|
||||||
|
elseif ($action === 'protocols' && $method === 'GET') echo json_encode($pdo->query("SELECT * FROM protocols")->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
elseif ($action === 'settings' && $method === 'GET') { $stmt = $pdo->query("SELECT * FROM settings"); $s = []; foreach($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) $s[$r['key_name']] = $r['value']; echo json_encode($s); }
|
||||||
|
elseif ($isAdmin) {
|
||||||
|
if ($action === 'protocols') {
|
||||||
|
if ($method === 'POST') { $pdo->prepare("INSERT INTO protocols (proto_key, name, exec_path, args, icon) VALUES (:key, :name, :path, :args, :icon)")->execute(['key' => strtolower($input['proto_key']), 'name' => $input['name'], 'path' => $input['exec_path'], 'args' => $input['args'], 'icon' => $input['icon'] ?? 'bi-gear']); echo json_encode(["status" => "success"]); }
|
||||||
|
elseif ($method === 'DELETE') { $pdo->prepare("DELETE FROM protocols WHERE id = :id AND proto_key != 'web'")->execute(['id' => $input['id']]); echo json_encode(["status" => "success"]); }
|
||||||
|
}
|
||||||
|
elseif ($action === 'templates') {
|
||||||
|
if ($method === 'POST') { $pdo->prepare("INSERT INTO templates (category, name, type, host, port, username, password, kuma_url, tags) VALUES (:category, :name, :type, :host, :port, :username, :password, :kuma_url, :tags)")->execute(['category' => $input['category'], 'name' => $input['name'], 'type' => $input['type'] ?? 'rdp', 'host' => $input['host'] ?? '', 'port' => $input['port'] ?? '', 'username' => $input['username'] ?? '', 'password' => $input['password'] ?? '', 'kuma_url' => $input['kuma_url'] ?? '', 'tags' => $input['tags'] ?? '']); echo json_encode(["status" => "success"]); }
|
||||||
|
elseif ($method === 'DELETE') { $pdo->prepare("DELETE FROM templates WHERE id = :id")->execute(['id' => $input['id']]); echo json_encode(["status" => "success"]); }
|
||||||
|
}
|
||||||
|
elseif ($action === 'settings' && $method === 'POST') {
|
||||||
|
// Robustes UPSERT, funktioniert immer in SQLite
|
||||||
|
$stmt_check = $pdo->prepare("SELECT COUNT(*) FROM settings WHERE key_name = ?");
|
||||||
|
$stmt_insert = $pdo->prepare("INSERT INTO settings (key_name, value) VALUES (?, ?)");
|
||||||
|
$stmt_update = $pdo->prepare("UPDATE settings SET value = ? WHERE key_name = ?");
|
||||||
|
|
||||||
|
foreach($input as $key => $val) {
|
||||||
|
$stmt_check->execute([$key]);
|
||||||
|
if($stmt_check->fetchColumn() > 0) $stmt_update->execute([$val, $key]);
|
||||||
|
else $stmt_insert->execute([$key, $val]);
|
||||||
|
}
|
||||||
|
logAudit($pdo, $currentUser, 'SETTINGS_UPDATE', "Globale Einstellungen geändert.");
|
||||||
|
echo json_encode(["status" => "success"]);
|
||||||
|
}
|
||||||
|
} else { http_response_code(403); echo json_encode(["error" => "Berechtigung fehlt"]); }
|
||||||
|
|
||||||
|
} catch (Exception $e) { http_response_code(500); echo json_encode(["error" => $e->getMessage()]); }
|
||||||
|
?>
|
||||||
71
WebRDM/css/style.css
Normal file
71
WebRDM/css/style.css
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
:root {
|
||||||
|
--bg-body: #f3f3f3; --bg-sidebar: #ffffff; --bg-card: #ffffff;
|
||||||
|
--text-main: #333333; --text-muted: #666666; --border-color: #d8d8d8;
|
||||||
|
--primary-color: #0078d7; --primary-hover: #005a9e;
|
||||||
|
--item-hover: #e5f1fb; --item-active: #cce4f7; --label-color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg-body: #1e1e1e; --bg-sidebar: #252526; --bg-card: #2d2d30;
|
||||||
|
--text-main: #f1f1f1; --text-muted: #aaaaaa; --border-color: #3e3e42;
|
||||||
|
--primary-color: #3f98e7; --primary-hover: #60a5fa;
|
||||||
|
--item-hover: #333333; --item-active: #094771; --label-color: #569cd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
body { height: 100vh; display: flex; flex-direction: column; overflow: hidden; background-color: var(--bg-body); color: var(--text-main); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
|
||||||
|
.navbar { background-color: var(--bg-sidebar) !important; border-bottom: 1px solid var(--border-color); }
|
||||||
|
.navbar-brand { color: var(--text-main) !important; font-weight: 600; cursor: pointer; text-decoration: none; }
|
||||||
|
.sidebar { height: 100%; overflow-y: auto; background-color: var(--bg-sidebar); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; }
|
||||||
|
.main-content { height: 100%; padding: 0; overflow-y: auto; background-color: var(--bg-body); }
|
||||||
|
|
||||||
|
.form-control, .form-select, .form-control[type="file"]::file-selector-button { background-color: var(--bg-sidebar); color: var(--text-main); border: 1px solid var(--border-color); border-radius: 4px; }
|
||||||
|
.form-control:focus, .form-select:focus { background-color: var(--bg-sidebar); color: var(--text-main); border-color: var(--primary-color); box-shadow: none; }
|
||||||
|
.btn-primary { background-color: var(--primary-color); border-color: var(--primary-color); color: white;}
|
||||||
|
.btn-outline-secondary { color: var(--text-main); border-color: var(--border-color); }
|
||||||
|
.btn-outline-secondary:hover { background-color: var(--item-hover); color: var(--text-main); }
|
||||||
|
.btn-nav-icon { background: none; border: 1px solid var(--border-color); color: var(--text-main); border-radius: 4px; width: 35px; height: 35px; display: flex; align-items: center; justify-content: center; margin-left: 10px; transition: 0.2s; }
|
||||||
|
.btn-nav-icon:hover { background-color: var(--item-hover); }
|
||||||
|
|
||||||
|
.folder-header { font-weight: 600; margin-top: 1rem; margin-bottom: 0.5rem; color: var(--text-muted); font-size: 0.85rem; text-transform: uppercase; }
|
||||||
|
.connection-item { cursor: pointer; padding: 6px 10px; display: flex; align-items: center; gap: 8px; font-size: 0.9rem; }
|
||||||
|
.connection-item:hover { background-color: var(--item-hover); }
|
||||||
|
.connection-item.active { background-color: var(--item-active); border-left: 3px solid var(--primary-color); padding-left: 7px;}
|
||||||
|
.depth-0 { margin-left: 0px; } .depth-1 { margin-left: 20px; } .depth-2 { margin-left: 40px; }
|
||||||
|
|
||||||
|
/* Devolutions UI Layout */
|
||||||
|
.dev-action-sidebar { background-color: var(--bg-sidebar); border-right: 1px solid var(--border-color); padding: 15px; height: 100%; width: 200px; flex-shrink: 0; }
|
||||||
|
.dev-action-group { margin-bottom: 25px; }
|
||||||
|
.dev-action-title { font-weight: bold; font-size: 0.85rem; border-bottom: 1px solid var(--border-color); padding-bottom: 5px; margin-bottom: 15px; color: var(--text-main); }
|
||||||
|
.dev-action-btn { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; cursor: pointer; padding: 10px; border-radius: 4px; color: var(--text-main); text-decoration: none; }
|
||||||
|
.dev-action-btn:hover { background-color: var(--item-hover); }
|
||||||
|
.dev-action-btn i { font-size: 1.8rem; margin-bottom: 5px; }
|
||||||
|
.dev-main-view { flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
|
.dev-header { background-color: var(--bg-card); padding: 15px 20px; border-bottom: 1px solid var(--border-color); display: flex; align-items: center; gap: 15px; }
|
||||||
|
.dev-tabs { background-color: var(--bg-card); padding: 0 20px; border-bottom: 1px solid var(--border-color); display: flex; gap: 20px; font-size: 0.9rem; }
|
||||||
|
.dev-tab { padding: 10px 0; cursor: pointer; color: var(--text-main); border-bottom: 2px solid transparent; }
|
||||||
|
.dev-tab.active { border-bottom-color: var(--primary-color); font-weight: bold; }
|
||||||
|
.dev-content { padding: 20px; overflow-y: auto; flex-grow: 1; }
|
||||||
|
.dev-row { background-color: var(--bg-card); border: 1px solid var(--border-color); border-bottom: none; padding: 10px 20px; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.dev-row:last-child { border-bottom: 1px solid var(--border-color); }
|
||||||
|
.dev-row-label { color: var(--label-color); font-size: 0.85rem; margin-bottom: 2px; }
|
||||||
|
.dev-row-value { font-size: 0.95rem; color: var(--text-main); }
|
||||||
|
.dev-row-tools { display: flex; gap: 15px; color: var(--text-muted); font-size: 1.1rem; }
|
||||||
|
.dev-row-tools i { cursor: pointer; transition: color 0.2s; }
|
||||||
|
.dev-row-tools i:hover { color: var(--primary-color); }
|
||||||
|
.dashboard-card { background-color: var(--bg-card); border: 1px solid var(--border-color); border-radius: 4px; padding: 2rem; }
|
||||||
|
.text-muted-custom { color: var(--text-muted); }
|
||||||
|
.tag-badge { background-color: var(--item-hover); color: var(--primary-color); border: 1px solid var(--border-color); padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; margin-right: 5px; }
|
||||||
|
|
||||||
|
/* Map Specifics */
|
||||||
|
#mynetwork { flex-grow: 1; width: 100%; height: 100%; background-color: var(--bg-body); }
|
||||||
|
div.vis-tooltip { background-color: var(--bg-sidebar); border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; padding: 10px; font-family: 'Segoe UI', Tahoma, sans-serif; font-size: 0.85rem; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
|
||||||
|
|
||||||
|
/* Login Overlay */
|
||||||
|
#login-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: var(--bg-body); z-index: 9999; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.login-box { background-color: var(--bg-card); padding: 3rem; border-radius: 8px; border: 1px solid var(--border-color); box-shadow: 0 10px 25px rgba(0,0,0,0.1); width: 100%; max-width: 400px; text-align: center; }
|
||||||
|
|
||||||
|
/* Toasts */
|
||||||
|
#toast { visibility: hidden; min-width: 250px; background-color: #333; color: #fff; text-align: center; border-radius: 4px; padding: 10px; position: fixed; z-index: 10000; left: 50%; bottom: 30px; font-size: 0.9rem; transform: translateX(-50%); }
|
||||||
|
#toast.show { visibility: visible; animation: fadein 0.5s, fadeout 0.5s 2.5s; }
|
||||||
|
@keyframes fadein { from {bottom: 0; opacity: 0;} to {bottom: 30px; opacity: 1;} }
|
||||||
|
@keyframes fadeout { from {bottom: 30px; opacity: 1;} to {bottom: 0; opacity: 0;} }
|
||||||
BIN
WebRDM/db/rdm.db
Normal file
BIN
WebRDM/db/rdm.db
Normal file
Binary file not shown.
BIN
WebRDM/db/secret.key
Normal file
BIN
WebRDM/db/secret.key
Normal file
Binary file not shown.
64
WebRDM/index.html
Normal file
64
WebRDM/index.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Web RDM - Professional SOC</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- LOGIN SCREEN -->
|
||||||
|
<div id="login-overlay">
|
||||||
|
<div class="login-box">
|
||||||
|
<h3 class="mb-4"><i class="bi bi-shield-lock-fill text-primary"></i> Web RDM</h3>
|
||||||
|
<div id="login-error" class="alert alert-danger d-none small"></div>
|
||||||
|
|
||||||
|
<div id="step-login">
|
||||||
|
<input type="text" id="l_user" class="form-control mb-3" placeholder="Benutzername">
|
||||||
|
<input type="password" id="l_pass" class="form-control mb-4" placeholder="Passwort" onkeypress="if(event.key === 'Enter') doLogin()">
|
||||||
|
<button class="btn btn-primary w-100" onclick="doLogin()">Anmelden</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="step-2fa" class="d-none">
|
||||||
|
<p class="small text-muted-custom">Bitte gib deinen 6-stelligen Google Authenticator Code ein.</p>
|
||||||
|
<input type="text" id="l_2fa" class="form-control mb-4 text-center fs-4" placeholder="000000" maxlength="6" onkeypress="if(event.key === 'Enter') verify2FA()">
|
||||||
|
<button class="btn btn-primary w-100" onclick="verify2FA()">Verifizieren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="navbar py-2">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<span class="navbar-brand mb-0 h1" onclick="showHomeDashboard()"><i class="bi bi-shield-lock-fill text-primary"></i> Web RDM</span>
|
||||||
|
<div class="d-flex gap-2 align-items-center">
|
||||||
|
<span class="badge bg-secondary me-2" id="user-badge"></span>
|
||||||
|
<a href="map.html" class="btn btn-sm btn-outline-primary d-flex align-items-center"><i class="bi bi-diagram-3-fill me-2"></i> Karte</a>
|
||||||
|
<button class="btn-nav-icon m-0" onclick="showSettings()" title="Einstellungen/Profil"><i class="bi bi-gear"></i></button>
|
||||||
|
<button class="btn-nav-icon m-0" onclick="toggleTheme()" title="Tag/Nacht Modus"><i id="theme-icon" class="bi bi-moon"></i></button>
|
||||||
|
<button class="btn-nav-icon m-0 text-danger border-danger" onclick="doLogout()" title="Abmelden"><i class="bi bi-box-arrow-right"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid flex-grow-1" style="height: calc(100vh - 57px);">
|
||||||
|
<div class="row h-100">
|
||||||
|
<div class="col-md-3 col-lg-2 sidebar p-0">
|
||||||
|
<div class="p-3 border-bottom" style="border-color: var(--border-color)!important;">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" id="searchInput" class="form-control" placeholder="Suchen..." onkeyup="filterSidebar()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="sidebar-content" class="flex-grow-1 overflow-auto py-2"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9 col-lg-10 main-content d-flex" id="main-area"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast">Nachricht</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="js/app.js"?version=3></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
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();
|
||||||
50
WebRDM/map.html
Normal file
50
WebRDM/map.html
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Netzwerk Topologie - Web RDM</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- LOGIN SCREEN (Wird von app.js gesteuert) -->
|
||||||
|
<div id="login-overlay">
|
||||||
|
<div class="login-box">
|
||||||
|
<h3 class="mb-4"><i class="bi bi-shield-lock-fill text-primary"></i> Web RDM Map</h3>
|
||||||
|
<div id="login-error" class="alert alert-danger d-none small"></div>
|
||||||
|
<div id="step-login">
|
||||||
|
<input type="text" id="l_user" class="form-control mb-3" placeholder="Benutzername">
|
||||||
|
<input type="password" id="l_pass" class="form-control mb-4" placeholder="Passwort" onkeypress="if(event.key === 'Enter') doLogin()">
|
||||||
|
<button class="btn btn-primary w-100" onclick="doLogin()">Anmelden</button>
|
||||||
|
</div>
|
||||||
|
<div id="step-2fa" class="d-none">
|
||||||
|
<p class="small text-muted-custom">Bitte gib deinen 6-stelligen Google Authenticator Code ein.</p>
|
||||||
|
<input type="text" id="l_2fa" class="form-control mb-4 text-center fs-4" placeholder="000000" maxlength="6" onkeypress="if(event.key === 'Enter') verify2FA()">
|
||||||
|
<button class="btn btn-primary w-100" onclick="verify2FA()">Verifizieren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="navbar py-2">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a href="index.html" class="navbar-brand mb-0 h1">
|
||||||
|
<i class="bi bi-arrow-left-circle-fill text-primary me-2"></i> Zurück zum RDM
|
||||||
|
</a>
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<span class="badge bg-secondary" id="user-badge"></span>
|
||||||
|
<span class="text-muted small"><i class="bi bi-info-circle"></i> Physische Layer-1/2 Map. Knoten ziehen zum Sortieren.</span>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" onclick="toggleTheme()" title="Tag/Nacht Modus"><i id="theme-icon" class="bi bi-moon"></i></button>
|
||||||
|
<button class="btn-nav-icon m-0 text-danger border-danger" onclick="doLogout()" title="Abmelden"><i class="bi bi-box-arrow-right"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="mynetwork"></div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user