Zugangsdaten-Entitaet, Gruppen-Tabelle mit Aufklappansicht, Vor-/Nachname, Dashboard-Sektionen
- Zugangsdaten (SSH-Logins) als eigenstaendige, wiederverwendbare Entitaet statt direkt am Switch; inline Neuanlage beim Switch-Erstellen moeglich; automatische Migration bestehender Switch-Logins - Gruppen-Seite als Tabelle mit Aufklapp-Zeile fuer Rechte (Akkordeon), Admin als feste Systemzeile (Mitgliederverwaltung ueber is_admin), Standardgruppe 'Benutzer' mit allen Ansichtsrechten (devices.view, switches.view), automatische Zuordnung neuer/verwaister Benutzer - Users-Seite: Icon-Buttons statt Text, Bearbeiten+Passwortaenderung in einem Modal zusammengefuehrt, Gruppe/Admin-Zuweisung ueber eigenen Zuweisen-Button (wie Switch-Zuordnung bei Devices), Vor-/Nachname - Dashboard: einheitliche Kachelansicht mit/ohne Login, drei sortierte Abschnitte (Offline/Online/Deaktiviert), Kachel-Suchfilter, Bootstrap- artiges Grid (max. 6 Spalten), Aktivieren-Option im Popup fuer deaktivierte Geraete, Countdown serverseitig korrekt geseedet - Sidebar dauerhaft einklappbar (Desktop, persistent via localStorage) - Devices-Tabelle: Aktions-Buttons nebeneinander statt untereinander Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
{% block page_sub %}<div class="topbar-sub">{{ switches|length }} Switche</div>{% endblock %}
|
||||
{% block topbar_right %}
|
||||
{% if can_create %}
|
||||
<button type="button" class="btn btn-primary" data-open-modal="addSwitchModal">
|
||||
<button type="button" class="btn btn-primary" data-open-modal="addSwitchModal" onclick="resetCredentialChoice('add')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
Neuer Switch
|
||||
</button>
|
||||
@@ -29,18 +29,24 @@
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="data-table" id="switchesTable">
|
||||
<thead>
|
||||
<tr><th>Hostname</th><th>IP-Adresse</th><th>Username</th><th style="width:1%;">Aktionen</th></tr>
|
||||
<tr><th>Hostname</th><th>IP-Adresse</th><th>Zugangsdaten</th><th style="width:1%;">Aktionen</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in switches %}
|
||||
<tr>
|
||||
<td class="cell-name">{{ s['hostname'] }}</td>
|
||||
<td class="mono">{{ s['ip'] }}</td>
|
||||
<td>{{ s['username'] }}</td>
|
||||
<td>
|
||||
{% if s['credential_name'] %}
|
||||
{{ s['credential_name'] }} <span class="text-faint mono" style="font-size:11.5px;">({{ s['credential_username'] }})</span>
|
||||
{% else %}
|
||||
<span class="text-faint">— keine —</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="row-actions">
|
||||
{% if can_edit %}
|
||||
<button class="icon-btn" title="Bearbeiten" data-open-modal="editSwitchModal{{ loop.index }}">
|
||||
<button class="icon-btn" title="Bearbeiten" data-open-modal="editSwitchModal{{ loop.index }}" onclick="resetCredentialChoice('edit{{ loop.index }}')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/></svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -58,7 +64,7 @@
|
||||
{% if can_edit %}
|
||||
<div class="modal-overlay" id="editSwitchModal{{ loop.index }}">
|
||||
<div class="modal">
|
||||
<form method="post" onsubmit="return validateSwitchForm(this, '{{ loop.index }}');">
|
||||
<form method="post" onsubmit="return validateSwitchForm(this, 'edit{{ loop.index }}');">
|
||||
<input type="hidden" name="edit_switch" value="1">
|
||||
<input type="hidden" name="old_hostname" value="{{ s['hostname'] }}">
|
||||
<div class="modal-header">
|
||||
@@ -73,15 +79,29 @@
|
||||
<input type="text" name="ip" value="{{ s['ip'] }}" required placeholder="z.B. 192.168.1.100">
|
||||
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben.</div>
|
||||
</div>
|
||||
<div class="field"><label>Username</label>
|
||||
<input type="text" name="username" value="{{ s['username'] }}" required>
|
||||
<div class="field">
|
||||
<label>Zugangsdaten</label>
|
||||
<select name="credential_choice" class="credential-select" onchange="toggleNewCredentialFields(this)">
|
||||
{% for c in all_credentials %}
|
||||
<option value="{{ c['id'] }}" data-username="{{ c['username'] }}" {% if c['id'] == s['credential_id'] %}selected{% endif %}>{{ c['name'] }}</option>
|
||||
{% endfor %}
|
||||
<option value="new">+ Neue Zugangsdaten anlegen</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label>Neues Passwort</label>
|
||||
<input type="password" id="password_edit_{{ loop.index }}" name="password" placeholder="Nur bei Änderung ausfüllen">
|
||||
</div>
|
||||
<div class="field"><label>Passwort bestätigen</label>
|
||||
<input type="password" id="password_confirm_edit_{{ loop.index }}" name="password_confirm" placeholder="Bestätigen">
|
||||
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
|
||||
<div class="new-credential-fields hidden">
|
||||
<div class="field"><label>Name der Zugangsdaten</label>
|
||||
<input type="text" name="new_credential_name" placeholder="z.B. Lager-Switche">
|
||||
</div>
|
||||
<div class="field"><label>Username</label>
|
||||
<input type="text" name="new_credential_username" placeholder="z.B. admin">
|
||||
</div>
|
||||
<div class="field"><label>Passwort</label>
|
||||
<input type="password" id="password_edit{{ loop.index }}" name="new_credential_password">
|
||||
</div>
|
||||
<div class="field"><label>Passwort bestätigen</label>
|
||||
<input type="password" id="password_confirm_edit{{ loop.index }}" name="new_credential_password_confirm">
|
||||
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -122,15 +142,32 @@
|
||||
<input type="text" name="ip" required placeholder="z.B. 192.168.1.100">
|
||||
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben.</div>
|
||||
</div>
|
||||
<div class="field"><label>Username</label>
|
||||
<input type="text" name="username" required placeholder="z.B. admin">
|
||||
<div class="field">
|
||||
<label>Zugangsdaten</label>
|
||||
<select name="credential_choice" class="credential-select" onchange="toggleNewCredentialFields(this)">
|
||||
{% if not all_credentials %}<option value="new" selected>+ Neue Zugangsdaten anlegen</option>
|
||||
{% else %}
|
||||
{% for c in all_credentials %}
|
||||
<option value="{{ c['id'] }}" data-username="{{ c['username'] }}">{{ c['name'] }}</option>
|
||||
{% endfor %}
|
||||
<option value="new">+ Neue Zugangsdaten anlegen</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label>Passwort</label>
|
||||
<input type="password" id="password_add" name="password" required>
|
||||
</div>
|
||||
<div class="field"><label>Passwort bestätigen</label>
|
||||
<input type="password" id="password_confirm_add" name="password_confirm" required>
|
||||
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
|
||||
<div class="new-credential-fields {% if all_credentials %}hidden{% endif %}">
|
||||
<div class="field"><label>Name der Zugangsdaten</label>
|
||||
<input type="text" name="new_credential_name" placeholder="z.B. Lager-Switche">
|
||||
</div>
|
||||
<div class="field"><label>Username</label>
|
||||
<input type="text" name="new_credential_username" placeholder="z.B. admin">
|
||||
</div>
|
||||
<div class="field"><label>Passwort</label>
|
||||
<input type="password" id="password_add" name="new_credential_password">
|
||||
</div>
|
||||
<div class="field"><label>Passwort bestätigen</label>
|
||||
<input type="password" id="password_confirm_add" name="new_credential_password_confirm">
|
||||
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -171,6 +208,7 @@
|
||||
<div class="modal-footer">
|
||||
<p class="text-faint" style="font-size:11.5px; margin-right:auto;">
|
||||
Erste Verbindung? Bestätige den Host-Key mit „yes“, gib danach das Passwort ein — direkt hier im Terminal.
|
||||
Bei bestehenden Zugangsdaten ist das Passwort hier nicht bekannt (verschlüsselt gespeichert) — bitte manuell eingeben.
|
||||
</p>
|
||||
<button type="button" class="btn btn-secondary" data-close-modal onclick="closeTerminal()">Schließen</button>
|
||||
</div>
|
||||
@@ -184,6 +222,23 @@
|
||||
<script src="{{ url_for('static', filename='js/vendor/xterm.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/vendor/xterm-addon-fit.js') }}"></script>
|
||||
<script>
|
||||
// -------------------------------------------------------------------------
|
||||
// Zugangsdaten-Auswahl: "+ Neue Zugangsdaten anlegen" blendet die Felder ein
|
||||
// -------------------------------------------------------------------------
|
||||
function toggleNewCredentialFields(select) {
|
||||
const fields = select.closest(".modal-body").querySelector(".new-credential-fields");
|
||||
if (fields) fields.classList.toggle("hidden", select.value !== "new");
|
||||
}
|
||||
function resetCredentialChoice(id) {
|
||||
// Beim Öffnen sicherstellen, dass die "Neue Zugangsdaten"-Felder passend
|
||||
// zur aktuellen Auswahl ein-/ausgeblendet sind (relevant v.a. nach
|
||||
// vorherigem Umschalten auf "neu" ohne zu speichern).
|
||||
setTimeout(() => {
|
||||
const select = document.querySelector(`#${id === 'add' ? 'addSwitchModal' : 'editSwitchModal' + id.replace('edit','')} .credential-select`);
|
||||
if (select) toggleNewCredentialFields(select);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SSH-Verbindungstest (Web-Terminal via /ws/ssh_terminal)
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -221,13 +276,25 @@ function setTermStatus(text, cls) {
|
||||
el.innerText = text;
|
||||
}
|
||||
|
||||
function getCredentialInfo(form) {
|
||||
const select = form.querySelector(".credential-select");
|
||||
if (!select || select.value === "new") {
|
||||
return {
|
||||
username: (form.querySelector("input[name='new_credential_username']") || {}).value?.trim(),
|
||||
passwordInput: form.querySelector("input[name='new_credential_password']"),
|
||||
};
|
||||
}
|
||||
const option = select.selectedOptions[0];
|
||||
return { username: option ? option.dataset.username : null, passwordInput: null };
|
||||
}
|
||||
|
||||
function openTerminal(form) {
|
||||
const host = (form.querySelector("input[name='ip']") || {}).value?.trim();
|
||||
const username = (form.querySelector("input[name='username']") || {}).value?.trim();
|
||||
activePasswordInput = form.querySelector("input[name='password']");
|
||||
const { username, passwordInput } = getCredentialInfo(form);
|
||||
activePasswordInput = passwordInput;
|
||||
|
||||
if (!host || !username) {
|
||||
showToast("Bitte IP-Adresse und Username ausfüllen, bevor du die Verbindung testest.", "danger");
|
||||
showToast("Bitte IP-Adresse ausfüllen und Zugangsdaten auswählen/anlegen, bevor du die Verbindung testest.", "danger");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -267,7 +334,7 @@ const termPasteBtn = document.getElementById("termPastePassword");
|
||||
if (termPasteBtn) {
|
||||
termPasteBtn.addEventListener("click", () => {
|
||||
if (!activePasswordInput || !activePasswordInput.value) {
|
||||
showToast("Kein Passwort im Formular eingetragen.", "danger");
|
||||
showToast("Kein Passwort bekannt — bitte manuell im Terminal eingeben.", "danger");
|
||||
return;
|
||||
}
|
||||
if (!termSocket || termSocket.readyState !== WebSocket.OPEN) {
|
||||
@@ -297,9 +364,11 @@ function validateIP(input) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
function validatePassword(id, isEdit) {
|
||||
const pass = document.getElementById(isEdit ? `password_edit_${id}` : "password_add");
|
||||
const confirm = document.getElementById(isEdit ? `password_confirm_edit_${id}` : "password_confirm_add");
|
||||
function validateNewCredentialPassword(form) {
|
||||
const select = form.querySelector(".credential-select");
|
||||
if (!select || select.value !== "new") return true;
|
||||
const pass = form.querySelector("input[name='new_credential_password']");
|
||||
const confirm = form.querySelector("input[name='new_credential_password_confirm']");
|
||||
if (!pass || !confirm) return true;
|
||||
if (pass.value !== confirm.value) { confirm.classList.add("is-invalid"); return false; }
|
||||
confirm.classList.remove("is-invalid");
|
||||
@@ -310,15 +379,15 @@ function validateSwitchForm(form, id) {
|
||||
const ipInput = form.querySelector("input[name='ip']");
|
||||
let valid = true;
|
||||
if (ipInput) valid = validateIP(ipInput) && valid;
|
||||
valid = validatePassword(id, id !== "add") && valid;
|
||||
valid = validateNewCredentialPassword(form) && valid;
|
||||
return valid;
|
||||
}
|
||||
|
||||
document.addEventListener("input", (e) => {
|
||||
if (e.target.name === "ip") validateIP(e.target);
|
||||
if (e.target.id.startsWith("password_confirm")) {
|
||||
const id = e.target.id.replace("password_confirm_", "");
|
||||
const pass = document.getElementById("password_" + id);
|
||||
if (e.target.name === "new_credential_password_confirm") {
|
||||
const form = e.target.closest("form");
|
||||
const pass = form.querySelector("input[name='new_credential_password']");
|
||||
if (pass) e.target.classList.toggle("is-invalid", pass.value !== e.target.value);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user