Merge branch 'Radius/add-router-management-v1' into 'master'

Add router management functionality with GenieACS integration

See merge request fronk/thetool!1915
This commit is contained in:
Luca Haid
2025-12-02 05:19:32 +00:00
3 changed files with 1169 additions and 1 deletions
+414 -1
View File
@@ -105,6 +105,8 @@ Vue.component('radius-users', {
class="fa-duotone fa-circle-info"></i></button>
<button class="ghost-btn" @click="openTransferModal(item.username)" data-tooltip="Transfer Statistik"
data-tooltip-align="left"><i class="fa-duotone fa-chart-line"></i></button>
<!-- <button class="ghost-btn" @click="openRouterManagement(item)" data-tooltip="Router Management"-->
<!-- data-tooltip-align="left"><i class="fa-duotone fa-router"></i></button>-->
</td>
</template>
<template #observer>
@@ -393,6 +395,135 @@ Vue.component('radius-users', {
</div>
</div>
</radius-modal>
<radius-modal :show="showRouterModal" :title="'Router Management - ' + (routerData.username || '')" @close="closeRouterModal" modal-class="modal-card-wide">
<div class="modal-body-scrollable">
<div v-if="routerLoading" class="table-placeholder" style="min-height: 300px;">
<div class="btn-loader" style="width: 50px; height: 50px;"></div>
<div style="margin-top: 16px;">Router wird gesucht...</div>
</div>
<div v-else-if="!routerDevice" class="table-placeholder" style="min-height: 300px;">
<i class="fa-duotone fa-router-slash" style="font-size: 48px; opacity: 0.3;"></i>
<div style="margin-top: 16px;">Kein Router mit dieser IP gefunden</div>
</div>
<div v-else>
<div class="kv-redesign">
<div class="kv-row"><span class="kv-label">Hersteller</span><code class="kv-value">{{ routerDevice.deviceInfo.manufacturer || '—' }}</code></div>
<div class="kv-row"><span class="kv-label">Modell</span><code class="kv-value">{{ routerDevice.deviceInfo.productClass || '—' }}</code></div>
<div class="kv-row"><span class="kv-label">Hardware Version</span><code class="kv-value">{{ routerDevice.deviceInfo.hardwareVersion || '—' }}</code></div>
<div class="kv-row"><span class="kv-label">Software Version</span><code class="kv-value">{{ routerDevice.deviceInfo.softwareVersion || '—' }}</code></div>
<div class="kv-row"><span class="kv-label">Seriennummer</span><code class="kv-value">{{ routerDevice.deviceInfo.serialNumber || '—' }}</code></div>
<div class="kv-row"><span class="kv-label">Device ID</span>
<div class="kv-value inline-copy">
<code>{{ routerDevice.deviceId || '—' }}</code>
<button v-if="routerDevice.deviceId" class="icon-btn sm" @click="copy(routerDevice.deviceId, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
</div>
</div>
<div class="kv-row"><span class="kv-label">Externe IP</span>
<div class="kv-value inline-copy">
<code>{{ routerDevice.ip || '—' }}</code>
<button v-if="routerDevice.ip" class="icon-btn sm" @click="copy(routerDevice.ip, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
</div>
</div>
<div class="kv-row"><span class="kv-label">Management IP</span>
<div class="kv-value inline-copy">
<code>{{ routerDevice.managementIp || '—' }}</code>
<button v-if="routerDevice.managementIp" class="icon-btn sm" @click="copy(routerDevice.managementIp, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
</div>
</div>
<div class="kv-row"><span class="kv-label">Management Username</span>
<div class="kv-value inline-copy">
<code v-if="!loadingUsername">{{ managementUsername || '—' }}</code>
<div v-else class="skeleton-line" style="width: 100px; height: 16px;"></div>
<button v-if="managementUsername && !loadingUsername" class="icon-btn sm" @click="copy(managementUsername, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
<button class="icon-btn sm" @click="fetchManagementUsername" :disabled="loadingUsername" data-tooltip="Aktualisieren" data-tooltip-align="left">
<i class="fa-duotone fa-arrows-rotate" :class="{'fa-spin': loadingUsername}"></i>
</button>
</div>
</div>
</div>
<div class="mt-3">
<h4 style="margin-bottom: 12px; font-size: 14px; font-weight: 600;">Router Aktionen</h4>
<div class="grid g-3 cols-3">
<button class="ghost-btn" @click="refreshRouter" :disabled="routerActionLoading" style="padding: 12px;">
<i class="fa-duotone fa-arrows-rotate"></i> Aktualisieren
</button>
<button class="ghost-btn" @click="rebootRouter" :disabled="routerActionLoading" style="padding: 12px;">
<i class="fa-duotone fa-power-off"></i> Neustart
</button>
<button class="ghost-btn" @click="pingRouter" :disabled="routerActionLoading" style="padding: 12px;">
<i class="fa-duotone fa-signal-bars"></i> Ping
</button>
<button class="danger-btn" @click="confirmFactoryReset" :disabled="routerActionLoading" style="padding: 12px;">
<i class="fa-duotone fa-triangle-exclamation"></i> Factory Reset
</button>
<button class="ghost-btn" @click="showParameterModal = true" :disabled="routerActionLoading" style="padding: 12px;">
<i class="fa-duotone fa-sliders"></i> Parameter lesen
</button>
<button class="ghost-btn" @click="showSetParameterModal = true" :disabled="routerActionLoading" style="padding: 12px;">
<i class="fa-duotone fa-pen-to-square"></i> Parameter setzen
</button>
</div>
</div>
<div v-if="pingResult" class="mt-3">
<h4 style="margin-bottom: 12px; font-size: 14px; font-weight: 600;">Ping Ergebnis</h4>
<div class="kv-redesign">
<div class="kv-row"><span class="kv-label">Pakete gesendet</span><code class="kv-value">{{ pingResult.packetsTransmitted }}</code></div>
<div class="kv-row"><span class="kv-label">Pakete empfangen</span><code class="kv-value">{{ pingResult.packetsReceived }}</code></div>
<div class="kv-row"><span class="kv-label">Paketverlust</span><code class="kv-value">{{ pingResult.packetLoss }}%</code></div>
<div class="kv-row"><span class="kv-label">Min / Avg / Max</span><code class="kv-value">{{ pingResult.min }} / {{ pingResult.avg }} / {{ pingResult.max }} ms</code></div>
</div>
</div>
</div>
</div>
</radius-modal>
<radius-modal :show="showParameterModal" title="Parameter lesen" @close="showParameterModal = false">
<div>
<div class="field">
<label style="margin-bottom: 8px; font-size: 14px;">Parameter Name</label>
<div class="input-wrap">
<i class="fa-duotone fa-code input-icon"></i>
<input class="ri" type="text" v-model.trim="parameterName" placeholder="z.B. InternetGatewayDevice.User.1.Username">
</div>
<p class="muted small" style="margin-top: 4px;">Geben Sie den vollständigen Parameter-Pfad ein</p>
</div>
<div class="cluster" style="justify-content: flex-end; margin-top: 24px; gap: 12px;">
<button class="ghost-btn" @click="showParameterModal = false">Abbrechen</button>
<button class="primary-btn" @click="getParameter" :disabled="!parameterName || routerActionLoading">
<span v-if="!routerActionLoading">Lesen</span>
<span v-else class="btn-loader"></span>
</button>
</div>
</div>
</radius-modal>
<radius-modal :show="showSetParameterModal" title="Parameter setzen" @close="showSetParameterModal = false">
<div>
<div class="field">
<label style="margin-bottom: 8px; font-size: 14px;">Parameter Name</label>
<div class="input-wrap">
<i class="fa-duotone fa-code input-icon"></i>
<input class="ri" type="text" v-model.trim="setParameterName" placeholder="z.B. InternetGatewayDevice.User.1.Password">
</div>
</div>
<div class="field mt-2">
<label style="margin-bottom: 8px; font-size: 14px;">Wert</label>
<div class="input-wrap">
<i class="fa-duotone fa-input-text input-icon"></i>
<input class="ri" type="text" v-model.trim="setParameterValue" placeholder="Neuer Wert">
</div>
</div>
<div class="cluster" style="justify-content: flex-end; margin-top: 24px; gap: 12px;">
<button class="ghost-btn" @click="showSetParameterModal = false">Abbrechen</button>
<button class="primary-btn" @click="setParameter" :disabled="!setParameterName || !setParameterValue || routerActionLoading">
<span v-if="!routerActionLoading">Setzen</span>
<span v-else class="btn-loader"></span>
</button>
</div>
</div>
</radius-modal>
</div>
`,
data: () => ({
@@ -426,7 +557,20 @@ Vue.component('radius-users', {
showEmailModal: false,
recipientEmail: '',
showExtensionIdModal: false,
extensionId: 'jglijfiddilckddlmbnlojmmlahboffh'
extensionId: 'jglijfiddilckddlmbnlojmmlahboffh',
showRouterModal: false,
routerLoading: false,
routerActionLoading: false,
routerData: {},
routerDevice: null,
pingResult: null,
showParameterModal: false,
parameterName: '',
showSetParameterModal: false,
setParameterName: '',
setParameterValue: '',
managementUsername: '',
loadingUsername: false
}),
computed: {
hasFilters() {
@@ -821,6 +965,275 @@ Vue.component('radius-users', {
},
plugins: [chartBackgroundColorPlugin]
});
},
async openRouterManagement(item) {
this.showRouterModal = true;
this.routerLoading = true;
this.routerData = item;
this.routerDevice = null;
this.pingResult = null;
this.managementUsername = '';
try {
// First get the IP from radacct
const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=fetchRadacct&username=${encodeURIComponent(item.username)}`);
if (r.ok) {
const radacct = await r.json();
if (radacct && radacct.ip) {
// Now fetch device from GenieACS using this IP
const deviceResponse = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetDeviceByIp?ip=${encodeURIComponent(radacct.ip)}`);
if (deviceResponse.ok) {
const deviceData = await deviceResponse.json();
if (deviceData.success) {
this.routerDevice = deviceData;
// Automatically fetch management username
await this.fetchManagementUsername();
}
}
}
}
} catch (e) {
console.error('Error fetching router:', e);
window.notify('error', 'Fehler beim Laden des Routers');
}
this.routerLoading = false;
},
closeRouterModal() {
this.showRouterModal = false;
this.routerData = {};
this.routerDevice = null;
this.pingResult = null;
this.showParameterModal = false;
this.showSetParameterModal = false;
this.parameterName = '';
this.setParameterName = '';
this.setParameterValue = '';
this.managementUsername = '';
this.loadingUsername = false;
},
async refreshRouter() {
if (!this.routerDevice || !this.routerDevice.deviceId) return;
this.routerActionLoading = true;
try {
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsRefreshDevice`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({deviceId: this.routerDevice.deviceId})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
window.notify('success', 'Router-Aktualisierung gestartet');
} else {
window.notify('error', data.message || 'Fehler beim Aktualisieren');
}
}
} catch (e) {
console.error('Error refreshing router:', e);
window.notify('error', 'Fehler beim Aktualisieren des Routers');
}
this.routerActionLoading = false;
},
async rebootRouter() {
if (!this.routerDevice || !this.routerDevice.deviceId) return;
if (!confirm('Möchten Sie den Router wirklich neu starten?')) return;
this.routerActionLoading = true;
try {
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsRebootDevice`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({deviceId: this.routerDevice.deviceId})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
window.notify('success', 'Router-Neustart gestartet');
} else {
window.notify('error', data.message || 'Fehler beim Neustart');
}
}
} catch (e) {
console.error('Error rebooting router:', e);
window.notify('error', 'Fehler beim Neustarten des Routers');
}
this.routerActionLoading = false;
},
async pingRouter() {
if (!this.routerDevice) return;
// Use management IP for ping, fallback to external IP
const pingIp = this.routerDevice.managementIp || this.routerDevice.ip;
if (!pingIp) return;
this.routerActionLoading = true;
this.pingResult = null;
try {
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsPing?ip=${encodeURIComponent(pingIp)}`);
if (response.ok) {
const data = await response.json();
if (data.success && data.result) {
this.pingResult = data.result;
window.notify('success', 'Ping erfolgreich');
} else {
window.notify('error', 'Ping fehlgeschlagen');
}
}
} catch (e) {
console.error('Error pinging router:', e);
window.notify('error', 'Fehler beim Pingen des Routers');
}
this.routerActionLoading = false;
},
async confirmFactoryReset() {
if (!confirm('ACHTUNG: Möchten Sie den Router wirklich auf Werkseinstellungen zurücksetzen? Diese Aktion kann nicht rückgängig gemacht werden!')) return;
if (!confirm('Sind Sie sicher? Alle Einstellungen gehen verloren!')) return;
await this.factoryResetRouter();
},
async factoryResetRouter() {
if (!this.routerDevice || !this.routerDevice.deviceId) return;
this.routerActionLoading = true;
try {
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsFactoryReset`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({deviceId: this.routerDevice.deviceId})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
window.notify('success', 'Factory Reset gestartet');
} else {
window.notify('error', data.message || 'Fehler beim Factory Reset');
}
}
} catch (e) {
console.error('Error factory resetting router:', e);
window.notify('error', 'Fehler beim Factory Reset');
}
this.routerActionLoading = false;
},
async getParameter() {
if (!this.routerDevice || !this.routerDevice.deviceId || !this.parameterName) return;
this.routerActionLoading = true;
try {
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetParameters`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
deviceId: this.routerDevice.deviceId,
parameters: [this.parameterName]
})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
window.notify('success', 'Parameter-Abfrage gestartet. Überprüfen Sie das GenieACS-Interface für Ergebnisse.');
this.showParameterModal = false;
this.parameterName = '';
} else {
window.notify('error', data.message || 'Fehler beim Lesen des Parameters');
}
}
} catch (e) {
console.error('Error getting parameter:', e);
window.notify('error', 'Fehler beim Lesen des Parameters');
}
this.routerActionLoading = false;
},
async setParameter() {
if (!this.routerDevice || !this.routerDevice.deviceId || !this.setParameterName || !this.setParameterValue) return;
this.routerActionLoading = true;
try {
const parameters = {};
parameters[this.setParameterName] = this.setParameterValue;
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsSetParameters`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
deviceId: this.routerDevice.deviceId,
parameters: parameters
})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
window.notify('success', 'Parameter erfolgreich gesetzt');
this.showSetParameterModal = false;
this.setParameterName = '';
this.setParameterValue = '';
} else {
window.notify('error', data.message || 'Fehler beim Setzen des Parameters');
}
}
} catch (e) {
console.error('Error setting parameter:', e);
window.notify('error', 'Fehler beim Setzen des Parameters');
}
this.routerActionLoading = false;
},
async fetchManagementUsername() {
if (!this.routerDevice || !this.routerDevice.deviceId) return;
this.loadingUsername = true;
try {
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetParameters`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
deviceId: this.routerDevice.deviceId,
parameters: ['InternetGatewayDevice.User.1.Username']
})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
window.notify('success', 'Username-Abfrage gestartet. Bitte warten Sie einen Moment und klicken Sie erneut auf Aktualisieren.');
// Wait a bit and then fetch the device data to get the updated value
setTimeout(async () => {
try {
const deviceResponse = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetDeviceInfo?deviceId=${encodeURIComponent(this.routerDevice.deviceId)}`);
if (deviceResponse.ok) {
const deviceInfo = await deviceResponse.json();
if (deviceInfo.success && deviceInfo.fullData) {
// Extract username from device data
const usernameData = deviceInfo.fullData['InternetGatewayDevice.User.1.Username'];
if (usernameData && usernameData.value && usernameData.value[0]) {
this.managementUsername = usernameData.value[0];
}
}
}
} catch (e) {
console.error('Error fetching updated device info:', e);
} finally {
this.loadingUsername = false;
}
}, 3000);
} else {
this.loadingUsername = false;
window.notify('error', data.message || 'Fehler beim Lesen des Usernames');
}
}
} catch (e) {
console.error('Error fetching management username:', e);
window.notify('error', 'Fehler beim Lesen des Usernames');
this.loadingUsername = false;
}
}
}
});