Add user management and a settings page
This commit is contained in:
+450
-35
@@ -10,6 +10,8 @@ from aiohttp import web
|
||||
import jinja2
|
||||
from . import data
|
||||
from . import notify as notify_mod
|
||||
from . import settings as settings_mod
|
||||
from . import users as users_mod
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -20,6 +22,78 @@ def _render_template(html_str: str, **context) -> str:
|
||||
return tmpl.render(**context)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auth helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SESSION_COOKIE = "hbd_session"
|
||||
|
||||
|
||||
def _get_token(request) -> str:
|
||||
"""Extract session token from Bearer header, X-Auth-Token header, or cookie."""
|
||||
auth = request.headers.get("Authorization", "")
|
||||
if auth.lower().startswith("bearer "):
|
||||
return auth[7:].strip()
|
||||
header_token = request.headers.get("X-Auth-Token", "").strip()
|
||||
if header_token:
|
||||
return header_token
|
||||
return request.cookies.get(SESSION_COOKIE, "")
|
||||
|
||||
|
||||
def _current_user(request):
|
||||
"""Return the authenticated User, or None when auth is not enabled."""
|
||||
if not users_mod.users_enabled():
|
||||
return None # unauthenticated mode — all access allowed
|
||||
return users_mod.get_session_user(_get_token(request))
|
||||
|
||||
|
||||
def _require_auth(request):
|
||||
"""Return (user, None) or (None, error Response)."""
|
||||
if not users_mod.users_enabled():
|
||||
return None, None
|
||||
user = users_mod.get_session_user(_get_token(request))
|
||||
if user is None:
|
||||
return None, web.json_response({"error": "Unauthorized"}, status=401)
|
||||
return user, None
|
||||
|
||||
|
||||
def _require_auth_redirect(request):
|
||||
"""Like _require_auth but returns a redirect to /login for browser requests."""
|
||||
if not users_mod.users_enabled():
|
||||
return None, None
|
||||
user = users_mod.get_session_user(_get_token(request))
|
||||
if user is None:
|
||||
raise web.HTTPFound("/login")
|
||||
return user, None
|
||||
|
||||
|
||||
def _can_view_host(user, host) -> bool:
|
||||
"""Return True if *user* may see *host* (monitor or higher, or no auth)."""
|
||||
if user is None:
|
||||
return True
|
||||
if user.admin:
|
||||
return True
|
||||
return host.is_monitor(user.username)
|
||||
|
||||
|
||||
def _can_operate_host(user, host) -> bool:
|
||||
"""Manager-level: queue commands, DNS, upgrade."""
|
||||
if user is None:
|
||||
return True
|
||||
if user.admin:
|
||||
return True
|
||||
return host.is_manager(user.username)
|
||||
|
||||
|
||||
def _can_own_host(user, host) -> bool:
|
||||
"""Owner-level: drop host, transfer ownership."""
|
||||
if user is None:
|
||||
return True
|
||||
if user.admin:
|
||||
return True
|
||||
return host.is_owner(user.username)
|
||||
|
||||
|
||||
async def start(
|
||||
host: str,
|
||||
port: int,
|
||||
@@ -37,7 +111,8 @@ async def start(
|
||||
"""
|
||||
get_now = get_now or (lambda: time.time())
|
||||
|
||||
async def index(request):
|
||||
async def old_index(request):
|
||||
_require_auth_redirect(request)
|
||||
res = []
|
||||
res.append('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">')
|
||||
res.append("<html>")
|
||||
@@ -62,7 +137,15 @@ async def start(
|
||||
return web.Response(text=body, content_type="text/html")
|
||||
|
||||
async def api_hosts(request):
|
||||
lst = [hbdclass.Host.hosts[h].jsons() for h in hbdclass.Host.hosts]
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
hosts = [
|
||||
hbdclass.Host.hosts[h]
|
||||
for h in hbdclass.Host.hosts
|
||||
if _can_view_host(user, hbdclass.Host.hosts[h])
|
||||
]
|
||||
lst = [h.jsons() for h in hosts]
|
||||
return web.json_response(json.loads("[" + ",".join(lst) + "]"))
|
||||
|
||||
async def api_messages(request):
|
||||
@@ -70,6 +153,9 @@ async def start(
|
||||
return web.json_response(lst)
|
||||
|
||||
async def cmd(request):
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
qa = request.rel_url.query
|
||||
uname = qa.get("h")
|
||||
ucmd = qa.get("c")
|
||||
@@ -77,34 +163,50 @@ async def start(
|
||||
return web.Response(status=400, text="need h= and c= arguments")
|
||||
if uname not in hbdclass.Host.hosts:
|
||||
return web.Response(status=400, text=f"h={uname} not found")
|
||||
hbdclass.Host.hosts[uname].cmds.append(
|
||||
("CMD", {"cmd": urllib.parse.unquote(ucmd)})
|
||||
)
|
||||
host = hbdclass.Host.hosts[uname]
|
||||
if not _can_operate_host(user, host):
|
||||
return web.json_response({"error": "Forbidden"}, status=403)
|
||||
host.cmds.append(("CMD", {"cmd": urllib.parse.unquote(ucmd)}))
|
||||
return web.Response(text=f"cmd {uname} queued")
|
||||
|
||||
async def drop(request):
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
qa = request.rel_url.query
|
||||
uname = qa.get("h")
|
||||
if not uname:
|
||||
return web.Response(status=400, text="need h= argument")
|
||||
if uname not in hbdclass.Host.hosts:
|
||||
return web.Response(status=400, text=f"h={uname} not found")
|
||||
host = hbdclass.Host.hosts[uname]
|
||||
if not _can_own_host(user, host):
|
||||
return web.json_response({"error": "Forbidden"}, status=403)
|
||||
eventlog(uname, "INFO", "dropped")
|
||||
del hbdclass.Host.hosts[uname]
|
||||
return web.Response(text="Done")
|
||||
|
||||
async def register(request):
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
qa = request.rel_url.query
|
||||
uname = qa.get("h")
|
||||
if not uname:
|
||||
return web.Response(status=400, text="need h= argument")
|
||||
if uname not in hbdclass.Host.hosts:
|
||||
return web.Response(status=400, text=f"h={uname} not found")
|
||||
ll = hbdclass.Host.hosts[uname].registerDns()
|
||||
host = hbdclass.Host.hosts[uname]
|
||||
if not _can_operate_host(user, host):
|
||||
return web.json_response({"error": "Forbidden"}, status=403)
|
||||
ll = host.registerDns()
|
||||
eventlog(uname, "INFO", ll)
|
||||
return web.Response(text=str(ll))
|
||||
|
||||
async def update(request):
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
qa = request.rel_url.query
|
||||
uname = urllib.parse.unquote(qa.get("h", ""))
|
||||
ucode = qa.get("c")
|
||||
@@ -118,16 +220,21 @@ async def start(
|
||||
names = [n for n in hbdclass.Host.hosts]
|
||||
out = []
|
||||
for n in names:
|
||||
err = None
|
||||
host = hbdclass.Host.hosts[n]
|
||||
if not _can_operate_host(user, host):
|
||||
out.append(f"update skipped for {n}: Forbidden")
|
||||
continue
|
||||
op_err = None
|
||||
try:
|
||||
r = {"csum": None, "code": ucode}
|
||||
hbdclass.Host.hosts[n].cmds.append(("UPD", r))
|
||||
host.cmds.append(("UPD", r))
|
||||
except Exception as e:
|
||||
err = str(e)
|
||||
out.append(f"update started for {n}: {err if err else 'OK'}")
|
||||
op_err = str(e)
|
||||
out.append(f"update started for {n}: {op_err if op_err else 'OK'}")
|
||||
return web.Response(text="\n".join(out))
|
||||
|
||||
async def live(request):
|
||||
current_user, _ = _require_auth_redirect(request)
|
||||
# render template from hbd/templates/live.html using Jinja2
|
||||
# Resolve templates directory relative to the hbd package
|
||||
pkg_dir = os.path.dirname(__file__)
|
||||
@@ -151,6 +258,8 @@ async def start(
|
||||
hbdclass.Host.hosts[h].stateinfo() for h in sorted(hbdclass.Host.hosts)
|
||||
],
|
||||
messages=data.msgs[-30:],
|
||||
current_user=current_user.to_dict() if current_user else None,
|
||||
active_page="live",
|
||||
)
|
||||
return web.Response(text=body, content_type="text/html")
|
||||
|
||||
@@ -185,16 +294,18 @@ async def start(
|
||||
|
||||
async def api_host_plugins(request):
|
||||
"""Get all plugin data for a specific host."""
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
hostname = request.match_info.get("hostname")
|
||||
|
||||
if hostname not in hbdclass.Host.hosts:
|
||||
return web.json_response(
|
||||
{"error": f"Host '{hostname}' not found"},
|
||||
status=404
|
||||
)
|
||||
|
||||
return web.json_response({"error": f"Host '{hostname}' not found"}, status=404)
|
||||
|
||||
host = hbdclass.Host.hosts[hostname]
|
||||
|
||||
if not _can_view_host(user, host):
|
||||
return web.json_response({"error": "Forbidden"}, status=403)
|
||||
|
||||
# Get plugin data with most recent sample for each plugin
|
||||
plugins_summary = {}
|
||||
for plugin_name, samples in host.plugin_data.items():
|
||||
@@ -214,16 +325,18 @@ async def start(
|
||||
|
||||
async def api_host_plugin_detail(request):
|
||||
"""Get detailed data for a specific plugin on a host."""
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
hostname = request.match_info.get("hostname")
|
||||
plugin_name = request.match_info.get("plugin_name")
|
||||
|
||||
|
||||
if hostname not in hbdclass.Host.hosts:
|
||||
return web.json_response(
|
||||
{"error": f"Host '{hostname}' not found"},
|
||||
status=404
|
||||
)
|
||||
|
||||
return web.json_response({"error": f"Host '{hostname}' not found"}, status=404)
|
||||
|
||||
host = hbdclass.Host.hosts[hostname]
|
||||
if not _can_view_host(user, host):
|
||||
return web.json_response({"error": "Forbidden"}, status=403)
|
||||
|
||||
# Get limit from query parameter
|
||||
limit = request.rel_url.query.get("limit", "10")
|
||||
@@ -259,15 +372,17 @@ async def start(
|
||||
|
||||
async def api_host_alerts(request):
|
||||
"""Get alert states for a specific host."""
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
hostname = request.match_info.get("hostname")
|
||||
|
||||
|
||||
if hostname not in hbdclass.Host.hosts:
|
||||
return web.json_response(
|
||||
{"error": f"Host '{hostname}' not found"},
|
||||
status=404
|
||||
)
|
||||
|
||||
return web.json_response({"error": f"Host '{hostname}' not found"}, status=404)
|
||||
|
||||
host = hbdclass.Host.hosts[hostname]
|
||||
if not _can_view_host(user, host):
|
||||
return web.json_response({"error": "Forbidden"}, status=403)
|
||||
|
||||
# Get alert states
|
||||
alerts = []
|
||||
@@ -287,9 +402,14 @@ async def start(
|
||||
|
||||
async def api_all_alerts(request):
|
||||
"""Get all active alerts across all hosts."""
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
all_alerts = []
|
||||
|
||||
|
||||
for hostname, host in hbdclass.Host.hosts.items():
|
||||
if not _can_view_host(user, host):
|
||||
continue
|
||||
if threshold_checker:
|
||||
active_alerts = threshold_checker.get_active_alerts(host.alert_states)
|
||||
else:
|
||||
@@ -326,6 +446,9 @@ async def start(
|
||||
|
||||
async def api_acknowledge_alert(request):
|
||||
"""Acknowledge an alert to stop reminder notifications."""
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
@@ -350,7 +473,9 @@ async def start(
|
||||
)
|
||||
|
||||
host = hbdclass.Host.hosts[hostname]
|
||||
|
||||
if not _can_view_host(user, host):
|
||||
return web.json_response({"error": "Forbidden"}, status=403)
|
||||
|
||||
if metric_path not in host.alert_states:
|
||||
return web.json_response(
|
||||
{"error": f"Alert '{metric_path}' not found for host '{hostname}'"},
|
||||
@@ -373,50 +498,338 @@ async def start(
|
||||
|
||||
async def plugins_page(request):
|
||||
"""Render the plugin metrics visualization page."""
|
||||
current_user, _ = _require_auth_redirect(request)
|
||||
pkg_dir = os.path.dirname(__file__)
|
||||
templates_dir = config.get("templates_dir", os.path.join(pkg_dir, "templates"))
|
||||
env = jinja2.Environment(loader=jinja2.FileSystemLoader(templates_dir))
|
||||
|
||||
# Collect all hosts with plugin data
|
||||
|
||||
# Collect all hosts with plugin data (filtered by visibility)
|
||||
hosts_with_plugins = []
|
||||
for hostname in sorted(hbdclass.Host.hosts.keys()):
|
||||
host = hbdclass.Host.hosts[hostname]
|
||||
if not _can_view_host(current_user, host):
|
||||
continue
|
||||
if host.plugin_data:
|
||||
hosts_with_plugins.append({
|
||||
"name": hostname,
|
||||
"plugins": list(host.plugin_data.keys()),
|
||||
})
|
||||
|
||||
|
||||
tmpl = env.get_template("plugins.html")
|
||||
body = tmpl.render(
|
||||
title="Plugin Metrics - Heartbeat",
|
||||
header="Plugin Metrics",
|
||||
hosts=hosts_with_plugins,
|
||||
current_user=current_user.to_dict() if current_user else None,
|
||||
active_page="plugins",
|
||||
)
|
||||
return web.Response(text=body, content_type="text/html")
|
||||
|
||||
async def alerts_page(request):
|
||||
"""Render the alerts dashboard page."""
|
||||
current_user, _ = _require_auth_redirect(request)
|
||||
pkg_dir = os.path.dirname(__file__)
|
||||
templates_dir = config.get("templates_dir", os.path.join(pkg_dir, "templates"))
|
||||
env = jinja2.Environment(loader=jinja2.FileSystemLoader(templates_dir))
|
||||
|
||||
|
||||
tmpl = env.get_template("alerts.html")
|
||||
body = tmpl.render(
|
||||
title="Alerts Dashboard - Heartbeat",
|
||||
header="Alerts Dashboard",
|
||||
current_user=current_user.to_dict() if current_user else None,
|
||||
active_page="alerts",
|
||||
)
|
||||
return web.Response(text=body, content_type="text/html")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Auth endpoints
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
async def api_login(request):
|
||||
"""POST /api/0/auth/login {username, password} -> {token}
|
||||
Also sets an hbd_session cookie for browser clients.
|
||||
"""
|
||||
if not users_mod.users_enabled():
|
||||
return web.json_response({"error": "Auth not configured"}, status=404)
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
return web.json_response({"error": "Invalid JSON"}, status=400)
|
||||
username = body.get("username", "")
|
||||
password = body.get("password", "")
|
||||
user = users_mod.authenticate(username, password)
|
||||
if user is None:
|
||||
return web.json_response({"error": "Invalid credentials"}, status=401)
|
||||
token = users_mod.create_session(username)
|
||||
resp = web.json_response({"token": token, "username": username})
|
||||
resp.set_cookie(
|
||||
SESSION_COOKIE,
|
||||
token,
|
||||
max_age=users_mod.SESSION_TTL,
|
||||
httponly=True,
|
||||
samesite="Lax",
|
||||
)
|
||||
return resp
|
||||
|
||||
async def login_page(request):
|
||||
"""GET /login — show login form; POST /login — process and redirect."""
|
||||
if not users_mod.users_enabled():
|
||||
raise web.HTTPFound("/")
|
||||
|
||||
error = ""
|
||||
if request.method == "POST":
|
||||
form = await request.post()
|
||||
username = form.get("username", "")
|
||||
password = form.get("password", "")
|
||||
user = users_mod.authenticate(username, password)
|
||||
if user:
|
||||
token = users_mod.create_session(username)
|
||||
redirect_to = request.rel_url.query.get("next", "/")
|
||||
resp = web.HTTPFound(redirect_to)
|
||||
resp.set_cookie(
|
||||
SESSION_COOKIE,
|
||||
token,
|
||||
max_age=users_mod.SESSION_TTL,
|
||||
httponly=True,
|
||||
samesite="Lax",
|
||||
)
|
||||
raise resp
|
||||
error = "Invalid username or password."
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Heartbeat — Login</title>
|
||||
<style>
|
||||
body {{ font-family: sans-serif; background: #f5f5f5; display: flex;
|
||||
justify-content: center; align-items: center; height: 100vh; margin: 0; }}
|
||||
.box {{ background: #fff; padding: 2em 2.5em; border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,.15); min-width: 300px; }}
|
||||
h2 {{ margin: 0 0 1.2em; color: #333; font-size: 1.4em; }}
|
||||
label {{ display: block; margin-bottom: .3em; font-size: .9em; color: #555; }}
|
||||
input {{ width: 100%; padding: .5em .7em; border: 1px solid #ccc;
|
||||
border-radius: 4px; font-size: 1em; box-sizing: border-box; }}
|
||||
button {{ margin-top: 1.2em; width: 100%; padding: .6em; background: #0066cc;
|
||||
color: #fff; border: none; border-radius: 4px; font-size: 1em; cursor: pointer; }}
|
||||
button:hover {{ background: #0055aa; }}
|
||||
.error {{ color: #c00; font-size: .9em; margin-bottom: .8em; }}
|
||||
.field {{ margin-bottom: .9em; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<h2>Heartbeat</h2>
|
||||
{'<p class="error">' + error + '</p>' if error else ''}
|
||||
<form method="post">
|
||||
<div class="field"><label>Username</label><input name="username" autofocus></div>
|
||||
<div class="field"><label>Password</label><input name="password" type="password"></div>
|
||||
<button type="submit">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
return web.Response(text=html, content_type="text/html")
|
||||
|
||||
async def web_logout(request):
|
||||
"""GET /logout — clear session cookie and redirect to /login."""
|
||||
token = request.cookies.get(SESSION_COOKIE, "")
|
||||
users_mod.delete_session(token)
|
||||
resp = web.HTTPFound("/login")
|
||||
resp.del_cookie(SESSION_COOKIE)
|
||||
raise resp
|
||||
|
||||
async def api_logout(request):
|
||||
"""POST /api/0/auth/logout"""
|
||||
token = _get_token(request)
|
||||
users_mod.delete_session(token)
|
||||
resp = web.json_response({"success": True})
|
||||
resp.del_cookie(SESSION_COOKIE)
|
||||
return resp
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# User endpoints
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
async def api_user_avatar(request):
|
||||
"""GET /api/0/users/{username}/avatar — serve a local avatar file.
|
||||
|
||||
Only reachable when the user's avatar config value starts with '/'.
|
||||
Falls back to 404 for external URLs (the browser fetches those directly).
|
||||
"""
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
username = request.match_info.get("username")
|
||||
target_user = users_mod.get_user(username)
|
||||
if target_user is None:
|
||||
return web.Response(status=404, text="User not found")
|
||||
if not target_user.avatar_is_local():
|
||||
return web.Response(status=404, text="No local avatar configured")
|
||||
path = target_user.avatar
|
||||
if not os.path.isfile(path):
|
||||
return web.Response(status=404, text="Avatar file not found")
|
||||
# Infer content-type from extension
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
mime = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".svg": "image/svg+xml",
|
||||
}.get(ext, "application/octet-stream")
|
||||
return web.FileResponse(path=path, headers={"Content-Type": mime})
|
||||
|
||||
async def api_users(request):
|
||||
"""GET /api/0/users — admin only."""
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
if users_mod.users_enabled() and (user is None or not user.admin):
|
||||
return web.json_response({"error": "Forbidden"}, status=403)
|
||||
return web.json_response([u.to_dict() for u in users_mod.users.values()])
|
||||
|
||||
async def api_user_self(request):
|
||||
"""GET /api/0/users/me — own profile."""
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
if user is None:
|
||||
return web.json_response({"error": "Auth not configured"}, status=404)
|
||||
return web.json_response(user.to_dict())
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Host access endpoints
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
async def api_host_access_get(request):
|
||||
"""GET /api/0/hosts/{hostname}/access"""
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
hostname = request.match_info.get("hostname")
|
||||
if hostname not in hbdclass.Host.hosts:
|
||||
return web.json_response({"error": f"Host '{hostname}' not found"}, status=404)
|
||||
host = hbdclass.Host.hosts[hostname]
|
||||
if not _can_view_host(user, host):
|
||||
return web.json_response({"error": "Forbidden"}, status=403)
|
||||
return web.json_response(host.access_dict())
|
||||
|
||||
async def api_host_access_put(request):
|
||||
"""PUT /api/0/hosts/{hostname}/access — owner or admin only.
|
||||
|
||||
Body: {owner?: str, managers?: [str], monitors?: [str]}
|
||||
"""
|
||||
user, err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
hostname = request.match_info.get("hostname")
|
||||
if hostname not in hbdclass.Host.hosts:
|
||||
return web.json_response({"error": f"Host '{hostname}' not found"}, status=404)
|
||||
host = hbdclass.Host.hosts[hostname]
|
||||
if not _can_own_host(user, host):
|
||||
return web.json_response({"error": "Forbidden"}, status=403)
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
return web.json_response({"error": "Invalid JSON"}, status=400)
|
||||
|
||||
if "owner" in body:
|
||||
host.owner = body["owner"] or None
|
||||
if "managers" in body:
|
||||
host.managers = list(body["managers"])
|
||||
if "monitors" in body:
|
||||
host.monitors = list(body["monitors"])
|
||||
|
||||
return web.json_response(host.access_dict())
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# User profile page
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
async def profile_page(request):
|
||||
"""GET /profile — current user's settings and host access summary."""
|
||||
current_user, _ = _require_auth_redirect(request)
|
||||
pkg_dir = os.path.dirname(__file__)
|
||||
templates_dir = config.get("templates_dir", os.path.join(pkg_dir, "templates"))
|
||||
env = jinja2.Environment(loader=jinja2.FileSystemLoader(templates_dir))
|
||||
|
||||
# Build host access summary for this user
|
||||
owned, managed, monitored = [], [], []
|
||||
if current_user:
|
||||
for hostname, host in sorted(hbdclass.Host.hosts.items()):
|
||||
if host.is_owner(current_user.username):
|
||||
owned.append(hostname)
|
||||
elif host.is_manager(current_user.username):
|
||||
managed.append(hostname)
|
||||
elif host.is_monitor(current_user.username):
|
||||
monitored.append(hostname)
|
||||
|
||||
# Resolve notification channel configs for display
|
||||
notif_channels = []
|
||||
if current_user:
|
||||
for ch_name in (current_user.notification_channels or []):
|
||||
ch_cfg = config.get("notification_channels", {}).get(ch_name, {})
|
||||
notif_channels.append({"name": ch_name, "type": ch_cfg.get("type", "")})
|
||||
|
||||
tmpl = env.get_template("profile.html")
|
||||
body = tmpl.render(
|
||||
title="Profile - Heartbeat",
|
||||
header="My Profile",
|
||||
current_user=current_user.to_dict() if current_user else None,
|
||||
owned_hosts=owned,
|
||||
managed_hosts=managed,
|
||||
monitored_hosts=monitored,
|
||||
notification_channels=notif_channels,
|
||||
active_page="profile",
|
||||
)
|
||||
return web.Response(text=body, content_type="text/html")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Settings page (admin only)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
async def settings_page(request):
|
||||
"""GET /settings — read-only view of the current server configuration."""
|
||||
current_user, _ = _require_auth_redirect(request)
|
||||
if current_user and not current_user.admin:
|
||||
raise web.HTTPForbidden(reason="Admin access required")
|
||||
pkg_dir = os.path.dirname(__file__)
|
||||
templates_dir = config.get("templates_dir", os.path.join(pkg_dir, "templates"))
|
||||
env = jinja2.Environment(loader=jinja2.FileSystemLoader(templates_dir))
|
||||
tmpl = env.get_template("settings.html")
|
||||
body = tmpl.render(
|
||||
title="Settings - Heartbeat",
|
||||
sections=settings_mod.get_settings_sections(config),
|
||||
current_user=current_user.to_dict() if current_user else None,
|
||||
active_page="settings",
|
||||
)
|
||||
return web.Response(text=body, content_type="text/html")
|
||||
|
||||
app = web.Application()
|
||||
app.add_routes(
|
||||
[
|
||||
web.get("/", index),
|
||||
web.get("/", live),
|
||||
web.get("/old", old_index),
|
||||
# Auth
|
||||
web.get("/login", login_page),
|
||||
web.post("/login", login_page),
|
||||
web.get("/logout", web_logout),
|
||||
web.post("/api/0/auth/login", api_login),
|
||||
web.post("/api/0/auth/logout", api_logout),
|
||||
# Users
|
||||
web.get("/api/0/users", api_users),
|
||||
web.get("/api/0/users/me", api_user_self),
|
||||
web.get("/api/0/users/{username}/avatar", api_user_avatar),
|
||||
# Hosts
|
||||
web.get("/api/0/hosts", api_hosts),
|
||||
web.get("/api/0/messages", api_messages),
|
||||
web.get("/api/0/hosts/{hostname}/plugins", api_host_plugins),
|
||||
web.get("/api/0/hosts/{hostname}/plugins/{plugin_name}", api_host_plugin_detail),
|
||||
web.get("/api/0/hosts/{hostname}/alerts", api_host_alerts),
|
||||
web.get("/api/0/hosts/{hostname}/access", api_host_access_get),
|
||||
web.put("/api/0/hosts/{hostname}/access", api_host_access_put),
|
||||
web.get("/api/0/alerts", api_all_alerts),
|
||||
web.post("/api/0/alerts/acknowledge", api_acknowledge_alert),
|
||||
web.get("/c", cmd),
|
||||
@@ -426,6 +839,8 @@ async def start(
|
||||
web.get("/live", live),
|
||||
web.get("/plugins", plugins_page),
|
||||
web.get("/alerts", alerts_page),
|
||||
web.get("/profile", profile_page),
|
||||
web.get("/settings", settings_page),
|
||||
web.get("/static/{path:.*}", static),
|
||||
web.get("/favicon.ico", favicon),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user