🚧 WIP
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}MQTT Ingester{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<a class="brand" href="/data">MQTT Ingester</a>
|
||||
<a href="/data" {% if nav == 'data' %}class="active"{% endif %}>Data</a>
|
||||
{% if identity and identity.is_admin %}
|
||||
<a href="/users" {% if nav == 'users' %}class="active"{% endif %}>Users</a>
|
||||
<a href="/groups" {% if nav == 'groups' %}class="active"{% endif %}>Groups</a>
|
||||
{% endif %}
|
||||
<span class="spacer"></span>
|
||||
{% if identity %}
|
||||
<span class="user">{{ identity.username }}{% if identity.is_admin %} (admin){% endif %}</span>
|
||||
<a href="/password">Password</a>
|
||||
<form method="post" action="/logout" class="inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<button type="submit" class="link">Log out</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{% if identity and identity.must_change_password %}
|
||||
<div class="banner">Default password in use — please <a href="/password">change your password</a>.</div>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Data — MQTT Ingester{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Recent telemetry</h1>
|
||||
<form method="get" action="/data" class="filter">
|
||||
<input type="search" name="topic" value="{{ topic }}" placeholder="Filter by topic, e.g. 11001.*">
|
||||
<button type="submit">Filter</button>
|
||||
</form>
|
||||
<p class="hint">Showing the 100 most recent measurements you have access to. Patterns follow the access rules: <code>*</code> matches any sequence.</p>
|
||||
{% if rows %}
|
||||
<table>
|
||||
<thead><tr><th>Time</th><th>Topic</th><th>Payload</th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td>{{ row.time }}</td>
|
||||
<td>{{ row.topic }}</td>
|
||||
<td><code>{{ row.payload | tojson }}</code></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No telemetry visible (nothing ingested yet, or your access rules match no topics).</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ group.name }} — MQTT Ingester{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{{ group.name }}</h1>
|
||||
|
||||
<section>
|
||||
<h2>Access rules</h2>
|
||||
<p class="hint">Glob patterns matched against the full tag name. <code>*</code> matches any sequence (including <code>.</code>). Deny wins over allow. Group rules combine with users' personal rules.</p>
|
||||
<table>
|
||||
<thead><tr><th>Kind</th><th>Pattern</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rules %}
|
||||
<tr>
|
||||
<td><span class="tag {{ r.kind }}">{{ r.kind }}</span></td>
|
||||
<td><code>{{ r.pattern }}</code></td>
|
||||
<td>
|
||||
<form method="post" action="/groups/{{ group.id }}/rules/{{ r.id }}/delete">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<button type="submit" class="link danger">remove</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<form method="post" action="/groups/{{ group.id }}/rules" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<select name="kind">
|
||||
<option value="allow">allow</option>
|
||||
<option value="deny">deny</option>
|
||||
</select>
|
||||
<input type="text" name="pattern" placeholder="e.g. 11001.*" required>
|
||||
<button type="submit">Add rule</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Members</h2>
|
||||
<form method="post" action="/groups/{{ group.id }}/members" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<fieldset>
|
||||
{% for u in all_users %}
|
||||
<label class="check">
|
||||
<input type="checkbox" name="user_ids" value="{{ u.id }}"
|
||||
{% if u.id in members | map(attribute='id') | list %}checked{% endif %}>
|
||||
{{ u.username }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<button type="submit">Save members</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Groups — MQTT Ingester{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Groups</h1>
|
||||
<h2>Create group</h2>
|
||||
<form method="post" action="/groups" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<label>Name <input type="text" name="name" required></label>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
|
||||
<h2>Existing groups</h2>
|
||||
{% if groups %}
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Members</th><th>Rules</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for g in groups %}
|
||||
<tr>
|
||||
<td><a href="/groups/{{ g.id }}">{{ g.name }}</a></td>
|
||||
<td>{{ g.member_count }}</td>
|
||||
<td>{{ g.rule_count }}</td>
|
||||
<td>
|
||||
<form method="post" action="/groups/{{ g.id }}/delete">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<button type="submit" class="link danger">delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No groups yet.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Log in — MQTT Ingester</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body class="login-body">
|
||||
<main class="login-card">
|
||||
<h1>MQTT Ingester</h1>
|
||||
{% if error %}<p class="error">{{ error }}</p>{% endif %}
|
||||
<form method="post" action="/login">
|
||||
<label>Username
|
||||
<input type="text" name="username" required autofocus autocomplete="username">
|
||||
</label>
|
||||
<label>Password
|
||||
<input type="password" name="password" required autocomplete="current-password">
|
||||
</label>
|
||||
<button type="submit">Log in</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Password — MQTT Ingester{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Change password</h1>
|
||||
{% if error %}<p class="error">{{ error }}</p>{% endif %}
|
||||
{% if identity.must_change_password %}
|
||||
<p class="banner">You are using the default or admin-set password. Set a new one to continue — you will be redirected here until you do.</p>
|
||||
{% endif %}
|
||||
<form method="post" action="/password" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<label>Current password
|
||||
<input type="password" name="current_password" required>
|
||||
</label>
|
||||
<label>New password
|
||||
<input type="password" name="new_password" required minlength="8" autocomplete="new-password">
|
||||
</label>
|
||||
<label>Confirm new password
|
||||
<input type="password" name="confirm_password" required minlength="8" autocomplete="new-password">
|
||||
</label>
|
||||
<button type="submit">Change password</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ user.username }} — MQTT Ingester{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{{ user.username }} <span class="tag">{{ user.role }}</span></h1>
|
||||
{% if user.disabled_at %}<p class="banner">User is disabled.</p>{% endif %}
|
||||
|
||||
<section>
|
||||
<h2>Groups</h2>
|
||||
<form method="post" action="/users/{{ user.id }}/groups" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<fieldset>
|
||||
{% for g in all_groups %}
|
||||
<label class="check">
|
||||
<input type="checkbox" name="group_ids" value="{{ g.id }}"
|
||||
{% if g.id in member_of | map(attribute='id') | list %}checked{% endif %}>
|
||||
{{ g.name }}
|
||||
</label>
|
||||
{% else %}
|
||||
<p class="hint">No groups exist yet — create one under Groups.</p>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<button type="submit">Save groups</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Personal access rules</h2>
|
||||
<p class="hint">Glob patterns matched against the full tag name. <code>*</code> matches any sequence (including <code>.</code>). Deny wins over allow. Personal rules combine with group rules; use a personal deny to override a group allow, and vice versa.</p>
|
||||
<table>
|
||||
<thead><tr><th>Kind</th><th>Pattern</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rules %}
|
||||
<tr>
|
||||
<td><span class="tag {{ r.kind }}">{{ r.kind }}</span></td>
|
||||
<td><code>{{ r.pattern }}</code></td>
|
||||
<td>
|
||||
<form method="post" action="/users/{{ user.id }}/rules/{{ r.id }}/delete">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<button type="submit" class="link danger">remove</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<form method="post" action="/users/{{ user.id }}/rules" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<select name="kind">
|
||||
<option value="allow">allow</option>
|
||||
<option value="deny">deny</option>
|
||||
</select>
|
||||
<input type="text" name="pattern" placeholder="e.g. 11001.*_Value" required>
|
||||
<button type="submit">Add rule</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Set new password</h2>
|
||||
<form method="post" action="/users/{{ user.id }}/password" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<label>Password <input type="password" name="password" required minlength="8"></label>
|
||||
<button type="submit">Set password</button>
|
||||
</form>
|
||||
<p class="hint">The user must change it at next login.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Users — MQTT Ingester{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Users</h1>
|
||||
<h2>Create user</h2>
|
||||
<form method="post" action="/users" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<label>Username <input type="text" name="username" required></label>
|
||||
<label>Password <input type="password" name="password" required minlength="8"></label>
|
||||
<label>Role
|
||||
<select name="role">
|
||||
<option value="user">user</option>
|
||||
<option value="admin">admin</option>
|
||||
</select>
|
||||
</label>
|
||||
<fieldset>
|
||||
<legend>Groups</legend>
|
||||
{% for g in groups %}
|
||||
<label class="check"><input type="checkbox" name="group_ids" value="{{ g.id }}"> {{ g.name }}</label>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
|
||||
<h2>Existing users</h2>
|
||||
<table>
|
||||
<thead><tr><th>Username</th><th>Role</th><th>Groups</th><th>Status</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td><a href="/users/{{ u.id }}">{{ u.username }}</a></td>
|
||||
<td>{{ u.role }}</td>
|
||||
<td>{{ u.groups | join(', ') if u.groups else '—' }}</td>
|
||||
<td>
|
||||
{% if u.disabled_at %}<span class="tag disabled">disabled</span>
|
||||
{% elif u.must_change_password %}<span class="tag warn">must change pw</span>
|
||||
{% else %}<span class="tag ok">active</span>{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
<form method="post" action="/users/{{ u.id }}/{% if u.disabled_at %}enable{% else %}disable{% endif %}">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<button type="submit" class="link">{{ 'enable' if u.disabled_at else 'disable' }}</button>
|
||||
</form>
|
||||
<form method="post" action="/users/{{ u.id }}/delete">
|
||||
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
|
||||
<button type="submit" class="link danger">delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user