feat: Nutzerverwaltung + CMDB + Wissensdatenbank (Grundgeruest)
This commit is contained in:
parent
8c089d4f67
commit
a07cbb77ab
467
app.py
467
app.py
|
|
@ -197,10 +197,37 @@ _NAV = [
|
||||||
("Assets", [("/assets", "Asset-Liste")]),
|
("Assets", [("/assets", "Asset-Liste")]),
|
||||||
("Administration", [
|
("Administration", [
|
||||||
("/admin", "Einstellungen"),
|
("/admin", "Einstellungen"),
|
||||||
|
("/admin/users", "Benutzer"),
|
||||||
("/admin/audit", "Audit-Log"),
|
("/admin/audit", "Audit-Log"),
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CI_TYPES = ["Server", "Software", "Lizenz", "Vertrag", "Netzwerkgeraet", "Sonstiges"]
|
||||||
|
CI_STATUS = ["Aktiv", "Inaktiv", "Wartung", "Ausgemustert"]
|
||||||
|
REL_TYPES = ["haengt ab von", "beinhaltet", "verbunden mit", "ersetzt"]
|
||||||
|
KB_KATEGORIEN = ["Allgemein", "Anleitung", "Stoerung", "Konfiguration", "FAQ"]
|
||||||
|
|
||||||
|
|
||||||
|
def _attrs_to_text(attrs):
|
||||||
|
"""dict -> 'Schluessel: Wert' je Zeile, fuer die Textarea im Formular."""
|
||||||
|
if not attrs:
|
||||||
|
return ""
|
||||||
|
return "\n".join("%s: %s" % (k, v) for k, v in attrs.items())
|
||||||
|
|
||||||
|
|
||||||
|
def _text_to_attrs(text):
|
||||||
|
"""'Schluessel: Wert' je Zeile -> dict. Leere/fehlerhafte Zeilen werden ignoriert."""
|
||||||
|
out = {}
|
||||||
|
for line in (text or "").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or ":" not in line:
|
||||||
|
continue
|
||||||
|
k, v = line.split(":", 1)
|
||||||
|
k = k.strip()
|
||||||
|
if k:
|
||||||
|
out[k] = v.strip()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _shell(active_path, title, body_html, extra_head=""):
|
def _shell(active_path, title, body_html, extra_head=""):
|
||||||
u = _current_user()
|
u = _current_user()
|
||||||
|
|
@ -758,10 +785,159 @@ def admin_audit_route():
|
||||||
return _shell("/admin/audit", "Audit-Log", body)
|
return _shell("/admin/audit", "Audit-Log", body)
|
||||||
|
|
||||||
|
|
||||||
# ── Platzhalterseiten ───────────────────────────────────────────────────────────
|
# ── Administration: Benutzerverwaltung ─────────────────────────────────────────
|
||||||
def _placeholder(title, note):
|
def _role_pill(role):
|
||||||
return '<h1 class="page-title">%s</h1><div class="panel"><p class="sz">%s</p></div>' % (
|
return '<span class="pill %s">%s</span>' % (html.escape(role), html.escape(role))
|
||||||
html.escape(title), html.escape(note))
|
|
||||||
|
|
||||||
|
def _users_body(tenant_id, notice=""):
|
||||||
|
users = db.list_users(tenant_id)
|
||||||
|
rows = []
|
||||||
|
for u in users:
|
||||||
|
status_pill = ('<span class="pill admin" style="background:#1a4a35;color:var(--ok)">aktiv</span>'
|
||||||
|
if u["active"] else
|
||||||
|
'<span class="pill" style="background:#4a1e22;color:var(--bad)">gesperrt</span>')
|
||||||
|
toggle_label = "Sperren" if u["active"] else "Entsperren"
|
||||||
|
role_opts = "".join(
|
||||||
|
'<option value="%s" %s>%s</option>' % (r, "selected" if r == u["role"] else "", r)
|
||||||
|
for r in ("admin", "agent")
|
||||||
|
)
|
||||||
|
rows.append("""<tr>
|
||||||
|
<td>%s</td>
|
||||||
|
<td>%s</td>
|
||||||
|
<td>%s</td>
|
||||||
|
<td class="sz">%s</td>
|
||||||
|
<td class="sz">%s</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="/admin/users/%s/role" style="display:inline">
|
||||||
|
<select name="role" onchange="this.form.submit()">%s</select>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/admin/users/%s/active" style="display:inline">
|
||||||
|
<button class="btn ghost" type="submit" style="margin-left:6px">%s</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>""" % (
|
||||||
|
html.escape(u["email"]), _role_pill(u["role"]), status_pill,
|
||||||
|
html.escape(u["auth_source"]),
|
||||||
|
u["last_login_at"].strftime("%Y-%m-%d %H:%M") if u["last_login_at"] else "nie",
|
||||||
|
u["id"], role_opts, u["id"], toggle_label,
|
||||||
|
))
|
||||||
|
table = """<table class="tickets"><thead><tr>
|
||||||
|
<th>E-Mail</th><th>Rolle</th><th>Status</th><th>Quelle</th><th>Letzte Anmeldung</th><th>Aktionen</th>
|
||||||
|
</tr></thead><tbody>%s</tbody></table>""" % ("".join(rows) or "<tr><td colspan='6' class='sz'>Keine Benutzer.</td></tr>")
|
||||||
|
|
||||||
|
add_form = """<div class="panel"><h3 style="margin-top:0">Neuen Benutzer anlegen</h3>
|
||||||
|
<div class="sz" style="margin-bottom:10px">Manuelle Verwaltung. Eine AD/LDAP-Anbindung ist als naechste Ausbaustufe vorgesehen (Spalte "Quelle" zeigt dann "ldap" statt "local").</div>
|
||||||
|
<form method="post" action="/admin/users">
|
||||||
|
<div class="grid-2">
|
||||||
|
<div class="formrow"><label>E-Mail</label><input type="email" name="email" required></div>
|
||||||
|
<div class="formrow"><label>Rolle</label><select name="role"><option value="agent">agent</option><option value="admin">admin</option></select></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-2">
|
||||||
|
<div class="formrow"><label>Passwort</label><input type="password" name="password" required></div>
|
||||||
|
<div class="formrow"><label>Passwort wiederholen</label><input type="password" name="password2" required></div>
|
||||||
|
</div>
|
||||||
|
<div class="hint">Mindestens 10 Zeichen. Der Benutzer sollte das Passwort nach der ersten Anmeldung selbst aendern.</div>
|
||||||
|
<button class="btn" type="submit">Anlegen</button>
|
||||||
|
</form></div>"""
|
||||||
|
|
||||||
|
return '<h1 class="page-title">Benutzerverwaltung</h1>' + notice + table + add_form
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/users", methods=["GET", "POST"])
|
||||||
|
def admin_users_route():
|
||||||
|
r = _require_admin()
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
tenant_id = session["tenant_id"]
|
||||||
|
notice = ""
|
||||||
|
if request.method == "POST":
|
||||||
|
email = request.form.get("email", "").strip().lower()
|
||||||
|
role = request.form.get("role", "agent").strip()
|
||||||
|
if role not in ("admin", "agent"):
|
||||||
|
role = "agent"
|
||||||
|
pw = request.form.get("password", "")
|
||||||
|
pw2 = request.form.get("password2", "")
|
||||||
|
if not email or "@" not in email:
|
||||||
|
notice = "<p class='err'>Bitte eine gueltige E-Mail-Adresse angeben.</p>"
|
||||||
|
elif len(pw) < 10:
|
||||||
|
notice = "<p class='err'>Das Passwort muss mindestens 10 Zeichen lang sein.</p>"
|
||||||
|
elif pw != pw2:
|
||||||
|
notice = "<p class='err'>Die Passwoerter stimmen nicht ueberein.</p>"
|
||||||
|
elif db.email_exists(email):
|
||||||
|
notice = "<p class='err'>Diese E-Mail-Adresse ist bereits registriert.</p>"
|
||||||
|
else:
|
||||||
|
uid = db.create_user(tenant_id, email, generate_password_hash(pw), role=role)
|
||||||
|
db.log_audit(tenant_id, session["user_id"], "user_created", "user", str(uid),
|
||||||
|
{"email": email, "role": role, "auth_source": "local"}, _client_ip())
|
||||||
|
notice = "<p class='sz' style='color:var(--ok)'>Benutzer angelegt.</p>"
|
||||||
|
return _shell("/admin/users", "Benutzerverwaltung", _users_body(tenant_id, notice))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/users/<int:user_id>/role", methods=["POST"])
|
||||||
|
def admin_users_role_route(user_id):
|
||||||
|
r = _require_admin()
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
tenant_id = session["tenant_id"]
|
||||||
|
role = request.form.get("role", "agent").strip()
|
||||||
|
if role not in ("admin", "agent"):
|
||||||
|
return redirect(url_for("admin_users_route"))
|
||||||
|
db.set_user_role(tenant_id, user_id, role)
|
||||||
|
db.log_audit(tenant_id, session["user_id"], "user_role_changed", "user", str(user_id),
|
||||||
|
{"role": role}, _client_ip())
|
||||||
|
return redirect(url_for("admin_users_route"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/users/<int:user_id>/active", methods=["POST"])
|
||||||
|
def admin_users_active_route(user_id):
|
||||||
|
r = _require_admin()
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
tenant_id = session["tenant_id"]
|
||||||
|
current = db.get_user(tenant_id, user_id)
|
||||||
|
if not current:
|
||||||
|
return redirect(url_for("admin_users_route"))
|
||||||
|
new_active = not current["active"]
|
||||||
|
if user_id == session["user_id"] and not new_active:
|
||||||
|
return redirect(url_for("admin_users_route")) # eigenes Konto nicht selbst sperren
|
||||||
|
db.set_user_active(tenant_id, user_id, new_active)
|
||||||
|
db.log_audit(tenant_id, session["user_id"],
|
||||||
|
"user_activated" if new_active else "user_deactivated",
|
||||||
|
"user", str(user_id), None, _client_ip())
|
||||||
|
return redirect(url_for("admin_users_route"))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Wissensdatenbank ────────────────────────────────────────────────────────────
|
||||||
|
def _kb_list_body(tenant_id):
|
||||||
|
kategorie = request.args.get("kategorie") or None
|
||||||
|
q = request.args.get("q") or None
|
||||||
|
articles = db.list_kb_articles(tenant_id, kategorie=kategorie, q=q)
|
||||||
|
tabs = "".join(
|
||||||
|
'<a href="/wissen?kategorie=%s" class="%s">%s</a>' % (
|
||||||
|
html.escape(k), "active" if kategorie == k else "", html.escape(k))
|
||||||
|
for k in KB_KATEGORIEN
|
||||||
|
)
|
||||||
|
tabs = '<a href="/wissen" class="%s">Alle</a>' % ("active" if not kategorie else "") + tabs
|
||||||
|
rows = []
|
||||||
|
for a in articles:
|
||||||
|
tag_pills = "".join('<span class="pill agent" style="margin-right:4px">%s</span>' % html.escape(t.strip())
|
||||||
|
for t in (a["tags"] or "").split(",") if t.strip())
|
||||||
|
rows.append("""<tr class="row" onclick="location.href='/wissen/%s'">
|
||||||
|
<td><b>%s</b></td><td><span class="pill">%s</span></td><td>%s</td>
|
||||||
|
<td class="sz">%s</td><td class="sz">%s</td></tr>""" % (
|
||||||
|
a["id"], html.escape(a["titel"]), html.escape(a["kategorie"]), tag_pills,
|
||||||
|
html.escape(a["autor_email"] or ""), a["updated_at"].strftime("%Y-%m-%d %H:%M")))
|
||||||
|
table = """<table class="tickets"><thead><tr>
|
||||||
|
<th>Titel</th><th>Kategorie</th><th>Tags</th><th>Autor</th><th>Aktualisiert</th>
|
||||||
|
</tr></thead><tbody>%s</tbody></table>""" % ("".join(rows) or "<tr><td colspan='5' class='sz'>Keine Artikel.</td></tr>")
|
||||||
|
search_form = """<form method="get" action="/wissen" style="margin-bottom:14px">
|
||||||
|
<input type="text" name="q" placeholder="Suche in Titel, Inhalt, Tags ..." value="%s" style="max-width:420px;display:inline-block">
|
||||||
|
<button class="btn ghost" type="submit">Suchen</button>
|
||||||
|
</form>""" % html.escape(q or "")
|
||||||
|
return ('<h1 class="page-title">Wissensdatenbank</h1>'
|
||||||
|
'<div class="tabs">%s</div>%s%s'
|
||||||
|
'<div style="margin-top:14px"><a class="btn" href="/wissen/new">+ Neuer Artikel</a></div>' % (tabs, search_form, table))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/wissen")
|
@app.route("/wissen")
|
||||||
|
|
@ -769,8 +945,141 @@ def wissen_route():
|
||||||
r = _require_login()
|
r = _require_login()
|
||||||
if r:
|
if r:
|
||||||
return r
|
return r
|
||||||
return _shell("/wissen", "Wissensdatenbank",
|
return _shell("/wissen", "Wissensdatenbank", _kb_list_body(session["tenant_id"]))
|
||||||
_placeholder("Wissensdatenbank", "Noch nicht aufgebaut -- folgt in einer naechsten Ausbaustufe."))
|
|
||||||
|
|
||||||
|
@app.route("/wissen/new", methods=["GET", "POST"])
|
||||||
|
def wissen_new_route():
|
||||||
|
r = _require_login()
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
tenant_id = session["tenant_id"]
|
||||||
|
error = None
|
||||||
|
titel, kategorie, inhalt, tags = "", "Allgemein", "", ""
|
||||||
|
if request.method == "POST":
|
||||||
|
titel = request.form.get("titel", "").strip()
|
||||||
|
kategorie = request.form.get("kategorie", "Allgemein").strip()
|
||||||
|
inhalt = request.form.get("inhalt", "").strip()
|
||||||
|
tags = request.form.get("tags", "").strip()
|
||||||
|
if not titel:
|
||||||
|
error = "Titel ist erforderlich."
|
||||||
|
elif not inhalt:
|
||||||
|
error = "Inhalt darf nicht leer sein."
|
||||||
|
else:
|
||||||
|
aid = db.create_kb_article(tenant_id, titel, kategorie, inhalt, tags, session["user_id"])
|
||||||
|
db.log_audit(tenant_id, session["user_id"], "kb_article_created", "kb_article", str(aid),
|
||||||
|
{"titel": titel}, _client_ip())
|
||||||
|
return redirect(url_for("wissen_detail_route", article_id=aid))
|
||||||
|
kat_opts = "".join('<option value="%s" %s>%s</option>' % (k, "selected" if k == kategorie else "", k)
|
||||||
|
for k in KB_KATEGORIEN)
|
||||||
|
err_html = "<p class='err'>%s</p>" % html.escape(error) if error else ""
|
||||||
|
body = """<h1 class="page-title">Neuer Wissensartikel</h1>
|
||||||
|
<div class="panel">%s<form method="post">
|
||||||
|
<div class="formrow"><label>Titel</label><input name="titel" value="%s" required></div>
|
||||||
|
<div class="formrow"><label>Kategorie</label><select name="kategorie">%s</select></div>
|
||||||
|
<div class="formrow"><label>Inhalt</label><textarea name="inhalt" style="min-height:220px" required>%s</textarea></div>
|
||||||
|
<div class="formrow"><label>Tags (kommagetrennt)</label><input name="tags" value="%s"></div>
|
||||||
|
<button class="btn" type="submit">Speichern</button>
|
||||||
|
</form></div>""" % (err_html, html.escape(titel), kat_opts, html.escape(inhalt), html.escape(tags))
|
||||||
|
return _shell("/wissen", "Neuer Wissensartikel", body)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/wissen/<int:article_id>")
|
||||||
|
def wissen_detail_route(article_id):
|
||||||
|
r = _require_login()
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
a = db.get_kb_article(session["tenant_id"], article_id)
|
||||||
|
if not a:
|
||||||
|
abort(404)
|
||||||
|
tag_pills = "".join('<span class="pill agent" style="margin-right:4px">%s</span>' % html.escape(t.strip())
|
||||||
|
for t in (a["tags"] or "").split(",") if t.strip())
|
||||||
|
body = """<h1 class="page-title">%s</h1>
|
||||||
|
<div class="sz" style="margin-bottom:14px">%s · %s · aktualisiert %s</div>
|
||||||
|
<div class="panel" style="white-space:pre-wrap">%s</div>
|
||||||
|
<div style="margin-bottom:14px">%s</div>
|
||||||
|
<a class="btn ghost" href="/wissen/%s/edit">Bearbeiten</a>
|
||||||
|
<a class="btn ghost" href="/wissen">Zurueck zur Uebersicht</a>""" % (
|
||||||
|
html.escape(a["titel"]), html.escape(a["kategorie"]), html.escape(a["autor_email"] or ""),
|
||||||
|
a["updated_at"].strftime("%Y-%m-%d %H:%M"), html.escape(a["inhalt"]), tag_pills, a["id"])
|
||||||
|
return _shell("/wissen", a["titel"], body)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/wissen/<int:article_id>/edit", methods=["GET", "POST"])
|
||||||
|
def wissen_edit_route(article_id):
|
||||||
|
r = _require_login()
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
tenant_id = session["tenant_id"]
|
||||||
|
a = db.get_kb_article(tenant_id, article_id)
|
||||||
|
if not a:
|
||||||
|
abort(404)
|
||||||
|
error = None
|
||||||
|
if request.method == "POST":
|
||||||
|
titel = request.form.get("titel", "").strip()
|
||||||
|
kategorie = request.form.get("kategorie", "Allgemein").strip()
|
||||||
|
inhalt = request.form.get("inhalt", "").strip()
|
||||||
|
tags = request.form.get("tags", "").strip()
|
||||||
|
if not titel or not inhalt:
|
||||||
|
error = "Titel und Inhalt duerfen nicht leer sein."
|
||||||
|
else:
|
||||||
|
db.update_kb_article(tenant_id, article_id, titel, kategorie, inhalt, tags)
|
||||||
|
db.log_audit(tenant_id, session["user_id"], "kb_article_updated", "kb_article", str(article_id),
|
||||||
|
{"titel": titel}, _client_ip())
|
||||||
|
return redirect(url_for("wissen_detail_route", article_id=article_id))
|
||||||
|
a = dict(a)
|
||||||
|
a.update({"titel": titel, "kategorie": kategorie, "inhalt": inhalt, "tags": tags})
|
||||||
|
kat_opts = "".join('<option value="%s" %s>%s</option>' % (k, "selected" if k == a["kategorie"] else "", k)
|
||||||
|
for k in KB_KATEGORIEN)
|
||||||
|
err_html = "<p class='err'>%s</p>" % html.escape(error) if error else ""
|
||||||
|
body = """<h1 class="page-title">Artikel bearbeiten</h1>
|
||||||
|
<div class="panel">%s<form method="post">
|
||||||
|
<div class="formrow"><label>Titel</label><input name="titel" value="%s" required></div>
|
||||||
|
<div class="formrow"><label>Kategorie</label><select name="kategorie">%s</select></div>
|
||||||
|
<div class="formrow"><label>Inhalt</label><textarea name="inhalt" style="min-height:220px" required>%s</textarea></div>
|
||||||
|
<div class="formrow"><label>Tags (kommagetrennt)</label><input name="tags" value="%s"></div>
|
||||||
|
<button class="btn" type="submit">Speichern</button>
|
||||||
|
<form method="post" action="/wissen/%s/delete" style="display:inline" onsubmit="return confirm(\'Artikel wirklich loeschen?\')">
|
||||||
|
<button class="btn ghost" type="submit" style="margin-left:8px">Loeschen</button>
|
||||||
|
</form>
|
||||||
|
</form></div>""" % (err_html, html.escape(a["titel"]), kat_opts, html.escape(a["inhalt"]), html.escape(a["tags"] or ""), article_id)
|
||||||
|
return _shell("/wissen", "Artikel bearbeiten", body)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/wissen/<int:article_id>/delete", methods=["POST"])
|
||||||
|
def wissen_delete_route(article_id):
|
||||||
|
r = _require_login()
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
tenant_id = session["tenant_id"]
|
||||||
|
db.delete_kb_article(tenant_id, article_id)
|
||||||
|
db.log_audit(tenant_id, session["user_id"], "kb_article_deleted", "kb_article", str(article_id), None, _client_ip())
|
||||||
|
return redirect(url_for("wissen_route"))
|
||||||
|
|
||||||
|
|
||||||
|
# ── CMDB (Configuration Management Database) ───────────────────────────────────
|
||||||
|
def _ci_status_pill(status):
|
||||||
|
cls = {"Aktiv": "geloest", "Inaktiv": "geschlossen", "Wartung": "bearbeitung", "Ausgemustert": "ueberfaellig"}.get(status, "offen")
|
||||||
|
return '<span class="pill %s">%s</span>' % (cls, html.escape(status))
|
||||||
|
|
||||||
|
|
||||||
|
def _cmdb_list_body(tenant_id):
|
||||||
|
ci_typ = request.args.get("typ") or None
|
||||||
|
cis = db.list_cis(tenant_id, ci_typ=ci_typ)
|
||||||
|
tabs = '<a href="/assets" class="%s">Alle</a>' % ("active" if not ci_typ else "")
|
||||||
|
tabs += "".join('<a href="/assets?typ=%s" class="%s">%s</a>' % (t, "active" if ci_typ == t else "", t) for t in CI_TYPES)
|
||||||
|
rows = []
|
||||||
|
for c in cis:
|
||||||
|
rows.append("""<tr class="row" onclick="location.href='/assets/%s'">
|
||||||
|
<td><b>%s</b></td><td>%s</td><td>%s</td><td class="sz">%s</td></tr>""" % (
|
||||||
|
c["id"], html.escape(c["name"]), html.escape(c["ci_typ"]), _ci_status_pill(c["status"]),
|
||||||
|
html.escape(c["beschreibung"] or "")))
|
||||||
|
table = """<table class="tickets"><thead><tr>
|
||||||
|
<th>Name</th><th>Typ</th><th>Status</th><th>Beschreibung</th>
|
||||||
|
</tr></thead><tbody>%s</tbody></table>""" % ("".join(rows) or "<tr><td colspan='4' class='sz'>Keine Configuration Items.</td></tr>")
|
||||||
|
return ('<h1 class="page-title">CMDB -- Configuration Items</h1>'
|
||||||
|
'<div class="tabs">%s</div>%s'
|
||||||
|
'<div style="margin-top:14px"><a class="btn" href="/assets/new">+ Neues CI</a></div>' % (tabs, table))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/assets")
|
@app.route("/assets")
|
||||||
|
|
@ -778,8 +1087,150 @@ def assets_route():
|
||||||
r = _require_login()
|
r = _require_login()
|
||||||
if r:
|
if r:
|
||||||
return r
|
return r
|
||||||
return _shell("/assets", "Asset-Liste",
|
return _shell("/assets", "CMDB", _cmdb_list_body(session["tenant_id"]))
|
||||||
_placeholder("Asset-Liste", "Noch nicht aufgebaut -- folgt in einer naechsten Ausbaustufe."))
|
|
||||||
|
|
||||||
|
@app.route("/assets/new", methods=["GET", "POST"])
|
||||||
|
def assets_new_route():
|
||||||
|
r = _require_login()
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
tenant_id = session["tenant_id"]
|
||||||
|
error = None
|
||||||
|
name, ci_typ, status, beschreibung, attr_text = "", "Server", "Aktiv", "", ""
|
||||||
|
if request.method == "POST":
|
||||||
|
name = request.form.get("name", "").strip()
|
||||||
|
ci_typ = request.form.get("ci_typ", "Sonstiges").strip()
|
||||||
|
status = request.form.get("status", "Aktiv").strip()
|
||||||
|
beschreibung = request.form.get("beschreibung", "").strip()
|
||||||
|
attr_text = request.form.get("attribute", "")
|
||||||
|
if not name:
|
||||||
|
error = "Name ist erforderlich."
|
||||||
|
else:
|
||||||
|
cid = db.create_ci(tenant_id, name, ci_typ, status, beschreibung, _text_to_attrs(attr_text))
|
||||||
|
db.log_audit(tenant_id, session["user_id"], "ci_created", "ci", str(cid),
|
||||||
|
{"name": name, "ci_typ": ci_typ}, _client_ip())
|
||||||
|
return redirect(url_for("assets_detail_route", ci_id=cid))
|
||||||
|
typ_opts = "".join('<option value="%s" %s>%s</option>' % (t, "selected" if t == ci_typ else "", t) for t in CI_TYPES)
|
||||||
|
status_opts = "".join('<option value="%s" %s>%s</option>' % (s, "selected" if s == status else "", s) for s in CI_STATUS)
|
||||||
|
err_html = "<p class='err'>%s</p>" % html.escape(error) if error else ""
|
||||||
|
body = """<h1 class="page-title">Neues Configuration Item</h1>
|
||||||
|
<div class="panel">%s<form method="post">
|
||||||
|
<div class="grid-2">
|
||||||
|
<div class="formrow"><label>Name</label><input name="name" value="%s" required></div>
|
||||||
|
<div class="formrow"><label>Typ</label><select name="ci_typ">%s</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-2">
|
||||||
|
<div class="formrow"><label>Status</label><select name="status">%s</select></div>
|
||||||
|
<div class="formrow"></div>
|
||||||
|
</div>
|
||||||
|
<div class="formrow"><label>Beschreibung</label><textarea name="beschreibung">%s</textarea></div>
|
||||||
|
<div class="formrow"><label>Attribute (ein "Schluessel: Wert" je Zeile, z. B. IP: 10.0.0.5)</label>
|
||||||
|
<textarea name="attribute" style="min-height:100px" placeholder="IP: 10.0.0.5 Hersteller: Dell Vertragsnummer: V-2026-001">%s</textarea></div>
|
||||||
|
<button class="btn" type="submit">Anlegen</button>
|
||||||
|
</form></div>""" % (err_html, html.escape(name), typ_opts, status_opts, html.escape(beschreibung), html.escape(attr_text))
|
||||||
|
return _shell("/assets", "Neues Configuration Item", body)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/assets/<int:ci_id>", methods=["GET", "POST"])
|
||||||
|
def assets_detail_route(ci_id):
|
||||||
|
r = _require_login()
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
tenant_id = session["tenant_id"]
|
||||||
|
error = None
|
||||||
|
if request.method == "POST":
|
||||||
|
name = request.form.get("name", "").strip()
|
||||||
|
ci_typ = request.form.get("ci_typ", "Sonstiges").strip()
|
||||||
|
status = request.form.get("status", "Aktiv").strip()
|
||||||
|
beschreibung = request.form.get("beschreibung", "").strip()
|
||||||
|
attr_text = request.form.get("attribute", "")
|
||||||
|
if not name:
|
||||||
|
error = "Name ist erforderlich."
|
||||||
|
else:
|
||||||
|
db.update_ci(tenant_id, ci_id, name, ci_typ, status, beschreibung, _text_to_attrs(attr_text))
|
||||||
|
db.log_audit(tenant_id, session["user_id"], "ci_updated", "ci", str(ci_id), {"name": name}, _client_ip())
|
||||||
|
c = db.get_ci(tenant_id, ci_id)
|
||||||
|
if not c:
|
||||||
|
abort(404)
|
||||||
|
typ_opts = "".join('<option value="%s" %s>%s</option>' % (t, "selected" if t == c["ci_typ"] else "", t) for t in CI_TYPES)
|
||||||
|
status_opts = "".join('<option value="%s" %s>%s</option>' % (s, "selected" if s == c["status"] else "", s) for s in CI_STATUS)
|
||||||
|
err_html = "<p class='err'>%s</p>" % html.escape(error) if error else ""
|
||||||
|
|
||||||
|
other_cis = [x for x in db.list_cis(tenant_id) if x["id"] != ci_id]
|
||||||
|
ci_opts = "".join('<option value="%s">%s (%s)</option>' % (x["id"], html.escape(x["name"]), x["ci_typ"]) for x in other_cis)
|
||||||
|
rel_typ_opts = "".join('<option value="%s">%s</option>' % (t, t) for t in REL_TYPES)
|
||||||
|
rel_out_rows = "".join(
|
||||||
|
"<div class='tl-item'>%s <b>%s</b> <form method='post' action='/assets/%s/relationships/%s/delete' style='display:inline'>"
|
||||||
|
"<button class='btn ghost' type='submit' style='margin-left:6px;padding:2px 8px;font-size:11px'>x</button></form></div>" % (
|
||||||
|
html.escape(r["beziehungs_typ"]), html.escape(r["to_name"]), ci_id, r["id"])
|
||||||
|
for r in c["rel_out"]
|
||||||
|
) or "<p class='sz'>Keine ausgehenden Beziehungen.</p>"
|
||||||
|
rel_in_rows = "".join(
|
||||||
|
"<div class='tl-item'>%s <b>%s</b> <span class='sz'>(eingehend)</span></div>" % (
|
||||||
|
html.escape(r["beziehungs_typ"]), html.escape(r["from_name"]))
|
||||||
|
for r in c["rel_in"]
|
||||||
|
) or "<p class='sz'>Keine eingehenden Beziehungen.</p>"
|
||||||
|
|
||||||
|
body = """<h1 class="page-title">%s</h1>
|
||||||
|
<div class="panel">%s<form method="post">
|
||||||
|
<div class="grid-2">
|
||||||
|
<div class="formrow"><label>Name</label><input name="name" value="%s" required></div>
|
||||||
|
<div class="formrow"><label>Typ</label><select name="ci_typ">%s</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-2">
|
||||||
|
<div class="formrow"><label>Status</label><select name="status">%s</select></div>
|
||||||
|
<div class="formrow"></div>
|
||||||
|
</div>
|
||||||
|
<div class="formrow"><label>Beschreibung</label><textarea name="beschreibung">%s</textarea></div>
|
||||||
|
<div class="formrow"><label>Attribute (ein "Schluessel: Wert" je Zeile)</label>
|
||||||
|
<textarea name="attribute" style="min-height:100px">%s</textarea></div>
|
||||||
|
<button class="btn" type="submit">Speichern</button>
|
||||||
|
</form></div>
|
||||||
|
<h2 class="section-title">Beziehungen</h2>
|
||||||
|
<div class="panel">
|
||||||
|
<h3 style="margin-top:0">Ausgehend</h3>%s
|
||||||
|
<h3>Eingehend</h3>%s
|
||||||
|
<h3>Neue Beziehung</h3>
|
||||||
|
<form method="post" action="/assets/%s/relationships">
|
||||||
|
<div class="grid-2">
|
||||||
|
<div class="formrow"><label>Beziehungstyp</label><select name="beziehungs_typ">%s</select></div>
|
||||||
|
<div class="formrow"><label>Ziel-CI</label><select name="to_ci_id">%s</select></div>
|
||||||
|
</div>
|
||||||
|
<button class="btn ghost" type="submit">Hinzufuegen</button>
|
||||||
|
</form>
|
||||||
|
</div>""" % (
|
||||||
|
html.escape(c["name"]), err_html, html.escape(c["name"]), typ_opts, status_opts,
|
||||||
|
html.escape(c["beschreibung"] or ""), _attrs_to_text(c["attribute"]),
|
||||||
|
rel_out_rows, rel_in_rows, ci_id, rel_typ_opts, ci_opts,
|
||||||
|
)
|
||||||
|
return _shell("/assets", c["name"], body)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/assets/<int:ci_id>/relationships", methods=["POST"])
|
||||||
|
def assets_relationship_add_route(ci_id):
|
||||||
|
r = _require_login()
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
tenant_id = session["tenant_id"]
|
||||||
|
to_ci_id = request.form.get("to_ci_id", type=int)
|
||||||
|
beziehungs_typ = request.form.get("beziehungs_typ", "haengt ab von").strip()
|
||||||
|
if to_ci_id and to_ci_id != ci_id:
|
||||||
|
rid = db.create_ci_relationship(tenant_id, ci_id, to_ci_id, beziehungs_typ)
|
||||||
|
db.log_audit(tenant_id, session["user_id"], "ci_relationship_created", "ci_relationship", str(rid),
|
||||||
|
{"from": ci_id, "to": to_ci_id, "typ": beziehungs_typ}, _client_ip())
|
||||||
|
return redirect(url_for("assets_detail_route", ci_id=ci_id))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/assets/<int:ci_id>/relationships/<int:rel_id>/delete", methods=["POST"])
|
||||||
|
def assets_relationship_delete_route(ci_id, rel_id):
|
||||||
|
r = _require_login()
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
tenant_id = session["tenant_id"]
|
||||||
|
db.delete_ci_relationship(tenant_id, rel_id)
|
||||||
|
db.log_audit(tenant_id, session["user_id"], "ci_relationship_deleted", "ci_relationship", str(rel_id), None, _client_ip())
|
||||||
|
return redirect(url_for("assets_detail_route", ci_id=ci_id))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue