Compare commits

..

No commits in common. "db8ee556df08619a2cf4a44b9310524cb683c0e9" and "a71f68ade3bb45d2b8d1b964807cc82661ef2870" have entirely different histories.

18 changed files with 196 additions and 479 deletions

View File

@ -10,7 +10,7 @@ parser.add_argument("--template_folder", default="templates", help="path to Flas
parser.add_argument("--static_folder", default="static", help="path to Flask static folder") parser.add_argument("--static_folder", default="static", help="path to Flask static folder")
parser.add_argument("--admin_file", help="Path to file for creating initial admin users") parser.add_argument("--admin_file", help="Path to file for creating initial admin users")
parser.add_argument("--log_file", default="/var/log/webinterface.log", help="Path to log file") parser.add_argument("--log_file", default="/var/log/webinterface.log", help="Path to log file")
parser.add_argument("--ldap_url", default="ldaps://ldap.imaginaerraum.de", parser.add_argument("--ldap_url", default="ldaps://do.imaginaerraum.de",
help="URL for LDAP server for alternative user authorization") help="URL for LDAP server for alternative user authorization")
parser.add_argument("--mqtt_host", default="10.10.21.2", help="IP address of MQTT broker") parser.add_argument("--mqtt_host", default="10.10.21.2", help="IP address of MQTT broker")
parser.add_argument("--port", default=80, help="Port for running the Flask server") parser.add_argument("--port", default=80, help="Port for running the Flask server")

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,73 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 13.204024 10.591578"
id="svg2"
height="10.591578mm"
width="13.204024mm"
version="1.0"
sodipodi:docname="iR.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1853"
inkscape:window-height="1025"
id="namedview8"
showgrid="false"
inkscape:zoom="4"
inkscape:cx="33.91598"
inkscape:cy="-6.5287442"
inkscape:window-x="67"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
fit-margin-bottom="1.5"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0" />
<metadata
id="metadata13">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4">
<pattern
y="0"
x="0"
height="6"
width="6"
patternUnits="userSpaceOnUse"
id="WMFhbasepattern" />
</defs>
<path
id="path7"
d="m 3.356704,7.0871845 -0.008,-0.04772 -0.07159,-0.06363 h -0.05568 -0.07159 l -0.07954,0.111357 -0.04772,0.151131 -0.127268,0.42157 -0.238626,0.47725 -0.167037,0.246581 -0.167038,0.182945 -0.182945,0.135219 -0.167037,0.0875 -0.174993,0.03977 H 1.7181 1.65446 l -0.111358,-0.03183 -0.103405,-0.0875 -0.05568,-0.174993 -0.008,-0.135219 0.008,-0.13522 0.04773,-0.278399 0.13522,-0.429525 0.111357,-0.294302 0.914731,-2.433977 0.03977,-0.09545 0.07954,-0.310213 0.0159,-0.182948 -0.0159,-0.206807 -0.143175,-0.373845 -0.1909,-0.222718 -0.159083,-0.111357 -0.1909,-0.0875 -0.222718,-0.03977 -0.111357,-0.008 -0.111361,0.008 -0.214763,0.03977 -0.28635,0.127268 L 0.763594,3.6350655 0.501106,3.9691415 0.2068,4.5179795 0.0159,5.0986315 0,5.1940815 l 0.008,0.03977 0.07159,0.07159 0.06364,0.008 0.07954,-0.008 0.07159,-0.111361 0.03183,-0.119313 0.127268,-0.389752 0.294302,-0.612474 0.254536,-0.318164 0.182944,-0.151131 0.182945,-0.09545 0.1909,-0.04772 0.09545,-0.008 h 0.09545 l 0.167038,0.111361 0.07159,0.174989 v 0.143175 l -0.01589,0.270443 -0.167038,0.556793 -0.0875,0.238626 -0.914731,2.433976 -0.06363,0.151127 -0.0875,0.326124 -0.008,0.174989 0.0159,0.214763 0.143175,0.3818 0.1909,0.222718 0.167038,0.111357 0.182944,0.07954 0.214763,0.04772 h 0.119313 0.111361 l 0.206807,-0.04772 0.28635,-0.119312 0.318168,-0.278395 0.262488,-0.334075 0.28635,-0.556794 0.1909,-0.5727 0.0159,-0.09545 z M 3.245347,0.7954355 v -0.07159 l -0.05568,-0.15113 -0.119313,-0.127268 -0.159082,-0.07159 -0.111361,-0.008 -0.111357,0.008 -0.222718,0.09545 -0.182945,0.174993 -0.111357,0.222718 -0.008,0.127265 0.008,0.111361 0.08749,0.167037 0.127268,0.103402 0.143175,0.04772 h 0.06363 l 0.135219,-0.008 0.230674,-0.111361 0.174989,-0.182944 0.103406,-0.214763 0.008,-0.111357 z"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.37578771"
inkscape:connector-curvature="0" />
<path
id="path9"
d="m 7.508782,4.8520635 h 0.477251 l 1.956726,3.038492 0.294306,0.461343 0.294302,0.453388 0.06364,0.09545 0.174989,0.06363 h 0.198855 1.805596 0.167038 l 0.1909,-0.05568 0.06364,-0.103405 0.008,-0.07954 -0.008,-0.07954 -0.103405,-0.11136 -0.0875,-0.03183 -0.0875,-0.02386 -0.1909,-0.103406 -0.302257,-0.238625 -0.580656,-0.652243 -0.278395,-0.36589 -0.135219,-0.1909 -0.851099,-1.224944 -0.636333,-0.99427 0.437481,-0.0875 0.61247,-0.214763 0.373845,-0.198852 0.318168,-0.262488 0.254532,-0.326123 0.1909,-0.397708 0.09545,-0.493157 0.008,-0.28635 v -0.167038 l -0.04772,-0.310213 -0.07954,-0.28635 -0.119312,-0.262488 -0.238626,-0.349986 -0.42157,-0.373845 -0.501112,-0.28635 L 10.260982,0.1988555 9.664419,0.063632 9.0599,0 H 8.765595 4.661241 4.486252 l -0.198855,0.05568 -0.06363,0.095452 -0.008,0.07954 0.008,0.07954 0.07159,0.09545 0.174989,0.04772 h 0.09545 l 0.262488,0.008 0.334075,0.05568 0.127268,0.111361 0.05568,0.111357 0.03183,0.230669 V 1.201123 7.75536 7.98603 L 5.345307,8.224655 5.289627,8.336016 5.162359,8.447373 4.828284,8.503053 H 4.565796 4.470346 l -0.174989,0.05568 -0.07159,0.09545 -0.008,0.07159 0.008,0.07954 0.06363,0.103405 0.198855,0.05568 h 0.174989 3.587334 0.167037 l 0.1909,-0.05568 0.06363,-0.103405 0.008,-0.07954 -0.008,-0.07159 -0.07159,-0.09545 -0.167038,-0.05568 H 8.312202 8.049714 L 7.795182,8.463283 7.675869,8.407603 7.564512,8.280335 7.516792,7.978077 7.508792,7.755359 V 4.852087 Z m 2.529427,-0.644288 0.127268,-0.182948 0.167038,-0.429526 0.07954,-0.453388 0.03183,-0.461339 v -0.222718 -0.238625 l -0.04772,-0.485206 -0.103405,-0.477251 -0.198852,-0.453387 -0.151131,-0.206808 0.23067,0.05568 0.437481,0.159082 0.302257,0.159082 0.294306,0.206811 0.254532,0.278395 0.198856,0.334075 0.111357,0.413619 0.008,0.238625 -0.008,0.159082 -0.03977,0.302257 -0.08749,0.278399 -0.135224,0.254532 -0.198852,0.23067 -0.270443,0.198855 -0.34203,0.167038 -0.413615,0.127268 -0.246581,0.04772 z M 7.508782,1.1613245 v -0.103401 l 0.03977,-0.238626 0.103405,-0.167037 0.119313,-0.0875 0.174993,-0.07159 0.23067,-0.03183 0.143175,-0.008 0.206807,0.008 0.365893,0.04773 0.310213,0.09545 0.262488,0.167037 0.206807,0.23067 0.15113,0.302257 0.103406,0.389756 0.05568,0.485206 v 0.278395 0.302257 l -0.04772,0.509068 -0.103402,0.389756 -0.182949,0.294302 -0.270439,0.198856 -0.373849,0.135219 -0.493157,0.07159 -0.620425,0.03183 -0.381801,0.008 V 1.1613685 Z M 5.735,8.5030285 5.79864,8.3121285 5.83841,7.9064655 V 7.7871535 1.1772355 1.0499675 L 5.79864,0.6443045 5.735,0.4534045 h 1.487432 l -0.05568,0.07954 -0.07159,0.17499 -0.03977,0.262487 v 0.151131 6.665598 0.119312 l 0.03977,0.405663 0.06364,0.1909 z m 2.783963,-3.650965 0.119313,-0.0159 0.127264,-0.008 0.342031,-0.0159 0.342031,-0.03977 0.302257,0.493158 1.113583,1.662424 0.644288,0.859051 0.429525,0.501113 0.206811,0.214762 H 10.873397 L 8.518963,4.8520785 Z"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.37578771"
inkscape:connector-curvature="0" />
</svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,84 +1,57 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block header %} {% block header %}
{% block title %}<h1>Nutzer Übersicht</h1>{% endblock %} {% block title %}<h1>Admin Übersicht</h1>{% endblock %}
<script src="../static/js/jquery-3.6.0.js"></script> <script src="../static/jquery-3.6.0.js"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<table class="table"> <table border="1">
<thead> <td>Username</td>
<th scope="col">Benutzer</th> <td>E-Mail</td>
<th scope="col">E-Mail</th> <td>Aktiv</td>
<th scope="col">Aktiv</th> <td>Aktionen</td>
<th scope="col">Admin</th> {% for data in admin_data %}
<th scope="col">Super-Admin</th> <tr>
<th scope="col">Aktionen</th> {% for field in ['username', 'email', 'active'] %}
</thead> <td>{{ data[field] if data[field] }}</td>
<tbody>
{% for data in admin_data %}
{% if data['active'] %}
<tr>
{% else %}
<tr style="background-color: lightgrey">
{% endif %}
{% for field in ['username', 'email', 'active', 'admin', 'super_admin'] %}
<th scope="row">{{ data[field] if data[field] }}</th>
{% endfor %}
<td>
{% if not data['super_admin'] %}
<a href="{{ url_for('admin_toggle_active', username=data['username']) }}"><img src="static/stop.png" title="Aktivieren/Deaktivieren" alt="Toggle active"></a>
<a href="{{ url_for('delete_admins', username=data['username']) }}"><img src="static/delete.png" title="Löschen" alt="Delete"></a>
{% endif %}
{% if data['admin'] %}
{% if not data['super_admin'] %}
<a href="{{ url_for('demote_admin', username=data['username']) }}"><img src="static/demote.png" title="Admin-Rechte widerrufen" alt="Demote"></a>
{% endif %}
{% else %}
<a href="{{ url_for('promote_admin', username=data['username']) }}"><img src="static/promote.png" title="Zu Admin machen" alt="Promote"></a>
{% endif %}
</td>
</tr>
{% endfor %} {% endfor %}
</tbody> <td>
<a href="{{ url_for('admin_toggle_active', username=data['username']) }}"><img src="static/stop.png" title="Aktivieren/Deaktivieren" alt="Toggle active"></a>
<a href="{{ url_for('delete_admins', username=data['username']) }}"><img src="static/delete.png" title="Löschen" alt="Delete"></a>
</td>
</tr>
{% endfor %}
</table> </table>
<div class="d-grid gap-3"> <div>
<div class="p-2 bg-light border"> <p>
<h3>Neuen Benutzer erstellen:</h3> Neuen Admin erstellen:
</p>
<form method="POST"> <form method="POST">
<table> <table>
{{ form.csrf_token }} {{ form.csrf_token }}
<tr> <tr>
<td>{{ form.name.label }}</td> <td>{{ form.name.label }}</td>
<td>{{ form.name(size=20) }}</td> <td>{{ form.name(size=20) }}</td>
</tr> </tr>
<tr> <tr>
<td>{{ form.email.label }}</td> <td>{{ form.email.label }}</td>
<td>{{ form.email(size=20) }}</td> <td>{{ form.email(size=20) }}</td>
</tr> </tr>
<tr> <tr>
<td></td> <td></td>
<td> <td>
<input type="submit" value="Abschicken"> <input type="submit" value="Abschicken">
</td> </td>
</tr> </tr>
</table> </table>
</form> </form>
</div>
<div class="p-2 bg-light border">
<h3>Nutzerdaten sichern:</h3>
<form action="{{ url_for('backup_user_datastore') }}" method="get">
<input type="submit" value="Download">
</form>
</div>
<div class="p-2 bg-light border">
<h3>Nutzerdaten wiederherstellen:</h3>
<form action="{{ url_for('restore_user_datastore') }}" method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value="Abschicken">
</form>
</div>
</div> </div>
<form action="{{ url_for('backup_user_datastore') }}" method="get">
<input type="submit" value="Admin Daten sichern">
</form>
<form action="{{ url_for('restore_user_datastore') }}" method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value="Admin Daten wiederherstellen">
</form>
{% endblock %} {% endblock %}

View File

@ -1,81 +1,30 @@
<!doctype html> <!doctype html>
<html lang="de"> <title>Space Token Administration - {% block title %}{% endblock %}</title>
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}"> <nav>
<ul>
<title>Space Token Administration - {% block title %}{% endblock %}</title> <li><a href="{{ url_for('door_lock') }}">Home</a>
</head> <li><a href="{{ url_for('register') }}">Token Registrierung</a>
<li><a href="{{ url_for('list_tokens') }}">Token Übersicht</a>
<body> <li><a href="{{ url_for('open_door') }}">Tür öffnen</a>
<nav class="navbar navbar-expand-lg navbar-light bg-light"> <li><a href="{{ url_for('close_door') }}">Tür schließen</a>
<div class="container-fluid"> {% if current_user.has_role('super_admin') %}
<a class="navbar-brand" href="{{ url_for('door_lock') }}"><img src="{{ url_for('static', filename='iR.svg') }}" alt="iR Logo"></a> <li><a href="{{ url_for('manage_admins') }}">Admins verwalten</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" {% endif %}
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> {% if current_user.is_authenticated %}
<span class="navbar-toggler-icon"></span> <li><a href="{{ url_for('security.change_password') }}">Passwort ändern</a>
</button> <li><a href="{{ url_for('security.logout') }}">Benutzer <span>{{ current_user.username }}</span> ausloggen</a>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> {% else %}
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <li><a href="{{ url_for('security.login') }}">Einloggen</a>
{% if current_user.is_authenticated %} {% endif %}
</ul>
{% if current_user.has_role('admin') %} </nav>
<li class="nav-item dropdown"> <section class="content">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" <header>
data-bs-toggle="dropdown" aria-expanded="false"> {% block header %}{% endblock %}
Tokens </header>
</a> {% for message in get_flashed_messages() %}
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <div class="flash">{{ message }}</div>
<a class="dropdown-item" href="{{ url_for('register') }}">Token Registrierung</a> {% endfor %}
<a class="dropdown-item" href="{{ url_for('list_tokens') }}">Token Übersicht</a> {% block content %}{% endblock %}
</div> </section>
</li>
{% endif %}
{% if current_user.has_role('super_admin') %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('manage_admins') }}">Benutzer verwalten</a>
</li>
{% endif %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" id="navbarDropdown" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
Benutzer <span>{{ current_user.username }}</span>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{{ url_for('security.change_password') }}">Passwort
ändern</a>
<a class="dropdown-item" href="{{ url_for('security.logout') }}">Ausloggen</a>
</div>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('security.login') }}">Einloggen</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<section class="content">
<div class="container">
<header>
{% block header %}{% endblock %}
</header>
{% for message in get_flashed_messages() %}
<div class="alert alert-primary" role="alert">
{{ message }}
</div>
{% endfor %}
{% block content %}{% endblock %}
</div>
</section>
<script src="../static/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block header %} {% block header %}
{% block title %}<h1>Token löschen</h1>{% endblock %} {% block title %}<h1>Token löschen</h1>{% endblock %}
<script src="../static/js/jquery-3.6.0.js"></script> <script src="../static/jquery-3.6.0.js"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -1,12 +1,12 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block header %} {% block header %}
{% block title %}<h1>Benutzer löschen</h1>{% endblock %} {% block title %}<h1>Admin Benutzer löschen</h1>{% endblock %}
<script src="../static/js/jquery-3.6.0.js"></script> <script src="../static/jquery-3.6.0.js"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div> <div>
Achtung, Benutzer '{{ username }}' wird gelöscht. Achtung, Admin '{{ username }}' wird gelöscht.
Bitte zur Bestätigung den Nutzernamen eingeben: Bitte zur Bestätigung den Nutzernamen eingeben:
<form method="POST"> <form method="POST">
<table> <table>

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block header %} {% block header %}
{% block title %}<h1>Token editieren</h1>{% endblock %} {% block title %}<h1>Token editieren</h1>{% endblock %}
<script src="../static/js/jquery-3.6.0.js"></script> <script src="../static/jquery-3.6.0.js"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -4,35 +4,16 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="row"> Zustand der Tür:
Zustand der Tür: {% if door_state == 'closed' %}
{% if door_state == 'closed' %} <div style="color: red">
<div style="color: red"> Abgeschlossen
Abgeschlossen
</div>
{% elif door_state == 'open' %}
<div style="color: limegreen">
Geöffnet
</div>
{% endif %}
</div>
<div class="row">
Position Drehgeber: {{ encoder_position }}
</div>
{% if current_user.is_authenticated %}
<div class="row">
<div class="col-3">
<a href="{{ url_for('open_door') }}" class="btn btn-success" role="button">Tür öffnen</a>
</div> </div>
<div class="col-1"></div> {% elif door_state == 'open' %}
<div class="col-3"> <div style="color: limegreen">
<a href="{{ url_for('close_door') }}" class="btn btn-danger" role="button">Tür schließen</a> Geöffnet
</div> </div>
<div class="col-5"></div>
</div>
{% endif %} {% endif %}
<br>
Position Drehgeber: {{ encoder_position }}
{% endblock %} {% endblock %}

View File

@ -1,58 +1,55 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block header %} {% block header %}
{% block title %}<h1>Token Registrierung</h1>{% endblock %} {% block title %}<h1>Token Registrierung</h1>{% endblock %}
<script src="../static/js/jquery-3.6.0.js"></script> <script src="../static/jquery-3.6.0.js"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if not token.vars %} {% if not token.vars %}
<div class="d-grid gap-3">
<div class="p-2 bg-light border">
Letzter gelesener unregistrierter Token: {{ token['token'] }} <br> Letzter gelesener unregistrierter Token: {{ token['token'] }} <br>
Gelesen: {{ token['timestamp']}} Gelesen: {{ token['timestamp']}}
<div>
<p>
RaumnutzerIn registrieren:
</p>
<form method="POST">
<table>
{{ form.csrf_token }}
<tr>
<td>{{ form.name.label }}</td>
<td>{{ form.name(size=20) }}</td>
</tr>
<tr>
<td>{{ form.email.label }}</td>
<td>{{ form.email(size=20) }}</td>
</tr>
<tr>
<td>{{ form.organization.label }}</td>
<td>{{ form.organization(size=20) }}</td>
</tr>
<tr>
<td>{{ form.limit_validity.label }}</td>
<td> {{ form.limit_validity() }}</td>
</tr>
<tr id="valid_thru_row" style="display: none">
<td>{{ form.valid_thru.label }} </td>
<td>{{ form.valid_thru() }}</td>
</tr>
<tr>
<td>{{ form.dsgvo.label }} </td>
<td> {{ form.dsgvo() }}</td>
</tr>
<tr>
<td></td>
<td>
<input type="submit" value="Abschicken">
</td>
</tr>
</table>
</form>
</div> </div>
<div class="p-2 bg-light border">
<h3>RaumnutzerIn registrieren:</h3>
<form method="POST">
<table>
{{ form.csrf_token }}
<tr>
<td>{{ form.name.label }}</td>
<td>{{ form.name(size=20) }}</td>
</tr>
<tr>
<td>{{ form.email.label }}</td>
<td>{{ form.email(size=20) }}</td>
</tr>
<tr>
<td>{{ form.organization.label }}</td>
<td>{{ form.organization(size=20) }}</td>
</tr>
<tr>
<td>{{ form.limit_validity.label }}</td>
<td> {{ form.limit_validity() }}</td>
</tr>
<tr id="valid_thru_row" style="display: none">
<td>{{ form.valid_thru.label }} </td>
<td>{{ form.valid_thru() }}</td>
</tr>
<tr>
<td>{{ form.dsgvo.label }} </td>
<td> {{ form.dsgvo() }}</td>
</tr>
<tr>
<td></td>
<td>
<input type="submit" value="Abschicken">
</td>
</tr>
</table>
</form>
</div>
</div>
{% else %} {% else %}
Keine unregistrierten Tokens in MQTT Nachrichten. Bitte Token scannen und die Seite neu laden. Keine unregistrierten Tokens in MQTT Nachrichten. Bitte Token scannen und die Seite neu laden.
{% endif %} {% endif %}

View File

@ -1 +0,0 @@
<!-- empty since messages are already displayed in the base.html template -->

View File

@ -1,31 +0,0 @@
{% extends "base.html" %}
{% block doc -%}
<!DOCTYPE html>
<html{% block html_attribs %}{% endblock html_attribs %}>
{%- block html %}
<head>
{%- block head %}
<title>{% block title %}{{ title|default }}{% endblock title %}</title>
{%- block metas %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{%- endblock metas %}
{%- block styles %}
{%- endblock styles %}
{%- endblock head %}
</head>
<body{% block body_attribs %}{% endblock body_attribs %}>
{% block body -%}
{% block navbar %}
{%- endblock navbar %}
{% block content -%}
{%- endblock content %}
{% block scripts %}
{%- endblock scripts %}
{%- endblock body %}
</body>
{%- endblock html %}
</html>
{% endblock doc -%}

View File

@ -2,20 +2,17 @@
{% block header %} {% block header %}
{% block title %}<h1>Token Übersicht</h1>{% endblock %} {% block title %}<h1>Token Übersicht</h1>{% endblock %}
<script src="../static/js/jquery-3.6.0.js"></script> <script src="../static/jquery-3.6.0.js"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<table class="table"> <table border="1">
<thead>
<td>Token</td> <td>Token</td>
<td>NutzerIn</td> <td>NutzerIn</td>
<td>Organisation</td> <td>Organisation</td>
<td>E-Mail</td> <td>E-Mail</td>
<td>Gültig bis</td> <td>Gültig bis</td>
<td>Aktionen</td> <td>Aktionen</td>
</thead>
<tbody>
{% for t, data in assigned_tokens.items() %} {% for t, data in assigned_tokens.items() %}
<tr> <tr>
<td>{{ t }}</td> <td>{{ t }}</td>
@ -41,7 +38,6 @@
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table> </table>
<form action="{{ url_for('backup_tokens') }}" method="get"> <form action="{{ url_for('backup_tokens') }}" method="get">
<input type="submit" value="Token Daten sichern"> <input type="submit" value="Token Daten sichern">

View File

@ -144,25 +144,24 @@ def create_application(config):
# LDAP # LDAP
ldap_server = ldap3.Server(config.ldap_url) ldap_server = ldap3.Server(config.ldap_url)
local_ldap_cache = {} # dict for caching LDAP authorization locally (stores username + hashed password)
def validate_ldap(username, password): def validate_ldap(username, password):
"""Validate the user and password through an LDAP server. """Validate the user and password through an LDAP server.
If the connection completes successfully the given user and password is authorized. If the connection completes successfully the given user and password is authorized and the password is stored
Then the permissions and additional information of the user are obtained through an LDAP search. locally for future authorization without internet connectivity.
The data is stored in a dict which will be used later to create/update the entry for the user in the local If the server is not reachable we check the password against a locally stored password (if the user previously
database. authorized through LDAP).
Parameters Parameters
---------- ----------
username : username for the LDAP server user : username for the LDAP server
password : password for the LDAP server password : password for the LDAP server
Returns Returns
------- -------
bool : result of the authorization process (True = success, False = failure) bool : result of the authorization process (True = success, False = failure)
dict : dictionary with information about an authorized user (contains username, email, hashed password,
roles)
""" """
try: try:
@ -170,77 +169,55 @@ def create_application(config):
password=password, auto_bind=True) password=password, auto_bind=True)
except ldap3.core.exceptions.LDAPBindError: except ldap3.core.exceptions.LDAPBindError:
# server reachable but user unauthorized -> fail # server reachable but user unauthorized -> fail
return False, None return False
except LDAPSocketOpenError: except LDAPSocketOpenError:
# server not reachable -> fail (but will try authorization from local database later) # server not reachable -> try cached authorization data
return False, None return user.username in local_ldap_cache and verify_password(password, local_ldap_cache[user.username])
except Exception as e: except Exception as e:
# for other Exceptions we just fail # for other Exceptions we just fail
return False, None return False
# get user data and permissions from LDAP server # TODO check if user has permission to edit tokens
new_user_data = {} lock_permission = con.search('ou=Users,dc=imaginaerraum,dc=de', f'(&(uid={user.username})(memberof=cn=Members,ou=Groups,dc=imaginaerraum,dc=de))')
new_user_data['username'] = username
new_user_data['password'] = hash_password(password)
new_user_data['roles'] = []
lock_permission = con.search('ou=Users,dc=imaginaerraum,dc=de',
f'(&(uid={username})(memberof=cn=Members,ou=Groups,dc=imaginaerraum,dc=de))',
attributes=ldap3.ALL_ATTRIBUTES)
if lock_permission:
new_user_data['email'] = con.entries[0].mail.value
else:
new_user_data['email'] = None
token_granting_permission = con.search('ou=Users,dc=imaginaerraum,dc=de', token_granting_permission = con.search('ou=Users,dc=imaginaerraum,dc=de',
f'(&(uid={username})(memberof=cn=Vorstand,ou=Groups,dc=imaginaerraum,dc=de))') f'(&(uid={user.username})(memberof=cn=Vorstand,ou=Groups,dc=imaginaerraum,dc=de))')
if token_granting_permission: # if LDAP authorization succeeds we cache the password locally (in memory) to allow LDAP authentication even if
new_user_data['roles'].append('admin') # the server is not reachable
local_ldap_cache[user.username] = hash_password(password)
return True, new_user_data return True
class ExtendedLoginForm(LoginForm): class ExtendedLoginForm(LoginForm):
email = StringField('Benutzername oder E-Mail', [Required()]) email = StringField('Benutzername oder E-Mail', [Required()])
password = PasswordField('Passwort', [Required()]) password = PasswordField('Passwort', [Required()])
remember = BooleanField('Login merken?')
def validate(self): def validate(self):
# search for user in the current database # try authorizing using LDAP
user = find_user(self.email.data) # authorization in LDAP uses username -> get username associated with email from the database
if user is not None: try:
# if a user is found we use that username for LDAP authentication # if an email (instead of a username) was entered for authentication we check if there already is a user
username = user.username # with that email in the database
else: validate_email(self.email.data)
# this means there is no user with that email in the database user = find_user(self.email.data)
# we assume that the username was entered instead of an email and use that for authentication with LDAP if user is not None:
username = user.username
else:
# this means there is no user with that email in the database
username = None
except EmailNotValidError:
# else we use the entered credentials as username
username = self.email.data username = self.email.data
# run LDAP authorization authorized = validate_ldap(username, self.password.data)
# if the authorization succeeds we also get the new_user_data dict which contains information about the
# user's permissions etc.
authorized, new_user_data = validate_ldap(username, self.password.data)
if authorized: if authorized:
if user is None:
# if there was no user in the database before we create a new user
user_datastore.create_user(username=new_user_data['username'], email=new_user_data['email'],
password=new_user_data['password'], roles=new_user_data['roles'])
logger.info(f"New admin user '{new_user_data['username']} <{new_user_data['email']}>' created after"
" successful LDAP authorization")
else: # for existing users we update permissions and password/email to stay up to date
user.email = new_user_data['email']
user.password = new_user_data['password']
for role in new_user_data['roles']:
user_datastore.add_role_to_user(user, role)
user_datastore.commit()
self.user = find_user(self.email.data)
logger.info(f"Admin user with credentials '{self.email.data}' authorized through LDAP") logger.info(f"Admin user with credentials '{self.email.data}' authorized through LDAP")
if not authorized: if not authorized:
# try authorizing locally using Flask security user datastore # try authorizing locally using Flask security user datastore
authorized = super(ExtendedLoginForm, self).validate() authorized = super(ExtendedLoginForm, self).validate()
if authorized: if authorized:
logger.info(f"Admin user with credentials '{self.email.data}' authorized through local database") logger.info(f"Admin user with credentials '{self.email.data}' authorized through local database")
# if any of the authorization methods is successful we authorize the user # if any of the authorization methods is successful we authorize the user
return authorized return authorized
@ -256,22 +233,21 @@ def create_application(config):
form = AdminCreationForm() form = AdminCreationForm()
if request.method == 'GET': if request.method == 'GET':
users = user_datastore.user_model.query.all() users = user_datastore.user_model.query.all()
admin_data = [{'username': u.username, 'email': u.email, 'active': u.is_active, admin_data = [{'username': u.username, 'email': u.email, 'active': u.is_active} for u in users]
'admin': u.has_role('admin'), 'super_admin': u.has_role('super_admin'),
} for u in users]
return render_template('admins.html', admin_data=admin_data, form=form) return render_template('admins.html', admin_data=admin_data, form=form)
elif form.validate(): elif form.validate():
if user_datastore.find_user(username=form.name.data) is not None or \ if user_datastore.find_user(username=form.name.data) is not None or \
user_datastore.find_user(email=form.email.data) is not None: user_datastore.find_user(email=form.email.data) is not None:
flash("Ein Benutzer mit diesem Nutzernamen oder dieser E-Mail-Adresse existiert bereits!") flash("A user with the same name or email is already registered!")
return redirect('/manage_admins') return redirect('/manage_admins')
else: else:
pw = secrets.token_urlsafe(16) pw = secrets.token_urlsafe(16)
new_user = user_datastore.create_user(username=form.name.data, email=form.email.data, new_user = user_datastore.create_user(username=form.name.data, email=form.email.data, roles=['admin'],
password=hash_password(pw)) password=hash_password(pw))
logger.info( logger.info(
f"Super admin {current_user.username} created new user account for {new_user.username} <{new_user.email}>") f"Super admin {current_user.username} created new admin account for {new_user.username} <{new_user.email}>")
flash(f"Ein Account für den Nutzer {new_user.username} wurde erstellt. Verwende das Passwort {pw} um den Nutzer einzuloggen.") flash(
f"An account for the new admin user {new_user.username} has been created. Use the randomly generated password {pw} to log in.")
db.session.commit() db.session.commit()
return redirect('/manage_admins') return redirect('/manage_admins')
@ -280,13 +256,13 @@ def create_application(config):
def delete_admins(username): def delete_admins(username):
user = user_datastore.find_user(username=username) user = user_datastore.find_user(username=username)
if user is None: if user is None:
flash(f"Ungültiger Nutzer {username}") flash(f"Invalid user {username}")
return redirect('/manage_admins') return redirect('/manage_admins')
if user.has_role('super_admin'): if user.has_role('super_admin'):
flash('Super-Admins können nicht gelöscht werden!') flash('Cannot delete super admins!')
return redirect('/manage_admins') return redirect('/manage_admins')
if user.is_active: if user.is_active:
flash('Aktive Nutzer können nicht gelöscht werden! Bitte den Benutzer zuerst deaktivieren.') flash('Cannot delete active users. Please deactivate user first!')
return redirect('/manage_admins') return redirect('/manage_admins')
# set up form for confirming deletion # set up form for confirming deletion
@ -295,15 +271,15 @@ def create_application(config):
if request.method == 'GET': if request.method == 'GET':
# return page asking the user to confirm delete # return page asking the user to confirm delete
return render_template('delete_user.html', username=username, form=form) return render_template('delete_admin.html', username=username, form=form)
elif form.validate(): elif form.validate():
user_datastore.delete_user(user) user_datastore.delete_user(user)
flash(f"Benutzer {username} wurde gelöscht.") flash(f"Username {username} was deleted.")
logger.info(f"Super admin {current_user.username} deleted admin user {username}") logger.info(f"Super admin {current_user.username} deleted admin user {username}")
db.session.commit() db.session.commit()
return redirect('/manage_admins') return redirect('/manage_admins')
else: else:
flash("Der eingegebene Nutzername stimmt nicht überein. Der Benutzer wurde nicht gelöscht!") flash("Username does not match. User was not deleted!")
return redirect('/manage_admins') return redirect('/manage_admins')
@app.route('/admin_toggle_active/<username>') @app.route('/admin_toggle_active/<username>')
@ -311,10 +287,10 @@ def create_application(config):
def admin_toggle_active(username): def admin_toggle_active(username):
user = user_datastore.find_user(username=username) user = user_datastore.find_user(username=username)
if user is None: if user is None:
flash(f"Ungültiger Nutzer {username}") flash(f"Invalid user {username}")
return redirect('/manage_admins') return redirect('/manage_admins')
if user.has_role('super_admin'): if user.has_role('super_admin'):
flash('Super-Admins können nicht deaktiviert werden!') flash('Cannot deactivate super admins!')
return redirect('/manage_admins') return redirect('/manage_admins')
user_datastore.toggle_active(user) user_datastore.toggle_active(user)
if user.is_active: if user.is_active:
@ -324,46 +300,12 @@ def create_application(config):
db.session.commit() db.session.commit()
return redirect('/manage_admins') return redirect('/manage_admins')
@app.route('/promote_admin/<username>')
@roles_required('super_admin')
def promote_admin(username):
user = user_datastore.find_user(username=username)
if user is None:
flash(f"Ungültiger Nutzer {username}")
return redirect('/manage_admins')
if user.has_role('admin'):
flash(f'Benutzer {username} hat bereits Admin-Rechte!')
return redirect('/manage_admins')
user_datastore.add_role_to_user(user, 'admin')
logger.info(f"Super admin {current_user.username} granted admin privileges to user {username}")
db.session.commit()
return redirect('/manage_admins')
@app.route('/demote_admin/<username>')
@roles_required('super_admin')
def demote_admin(username):
user = user_datastore.find_user(username=username)
if user is None:
flash(f"Ungültiger Nutzer {username}")
return redirect('/manage_admins')
if user.has_role('super_admin'):
flash(f'Benutzer {username} hat Super-Admin-Rechte und kann nicht verändert werden!')
return redirect('/manage_admins')
if user.has_role('admin'):
user_datastore.remove_role_from_user(user, 'admin')
logger.info(f"Super admin {current_user.username} revoked admin privileges of user {username}")
db.session.commit()
else:
flash(f'Benutzer {username} ist bereits kein Admin!')
return redirect('/manage_admins')
@app.route('/backup_user_datastore') @app.route('/backup_user_datastore')
@roles_required('super_admin') @roles_required('super_admin')
def backup_user_datastore(): def backup_user_datastore():
# get list of defined admin users for backup # get list of defined admin users for backup
users = user_datastore.user_model.query.all() users = user_datastore.user_model.query.all()
user_data = [{'username': u.username, 'email': u.email, 'active': u.is_active, 'password_hash': u.password, user_data = [{'username': u.username, 'email': u.email, 'active': u.is_active, 'password_hash': u.password}
'roles': [r.name for r in u.roles]}
for u in users if not u.has_role('super_admin')] for u in users if not u.has_role('super_admin')]
try: try:
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
@ -396,20 +338,18 @@ def create_application(config):
valid &= all(type(d) == dict for d in user_data) valid &= all(type(d) == dict for d in user_data)
if valid: if valid:
for d in user_data: for d in user_data:
entry_valid = set(d.keys()) == { 'active', 'email', 'password_hash', 'username', 'roles'} entry_valid = set(d.keys()) == { 'active', 'email', 'password_hash', 'username'}
entry_valid &= all(len(d[key]) > 0 for key in ['email', 'password_hash', 'username']) entry_valid &= all(len(d[key]) > 0 for key in ['email', 'password_hash', 'username'])
entry_valid &= type(d['active']) == bool entry_valid &= type(d['active']) == bool
entry_valid &= type(d['roles']) == list
validate_email(d['email']) validate_email(d['email'])
if entry_valid: if entry_valid:
existing_user = user_datastore.find_user(username=d['username'], email=d['email']) existing_user = user_datastore.find_user(username=d['username'], email=d['email'])
if existing_user is None: if existing_user is None:
user_datastore.create_user(username=d['username'], email=d['email'], user_datastore.create_user(username=d['username'], email=d['email'], password=d['password_hash'],
password=d['password_hash'], active=d['active'], roles=['admin'], active=d['active'])
roles=d['roles']) flash(f"Admin Account für Benutzer '{d['username']} wurde wiederhergestellt.")
flash(f"Account für Benutzer '{d['username']} wurde wiederhergestellt.")
else: else:
flash(f"Benutzer '{d['username']} existiert bereits. Eintrag wird übersprungen.") flash(f"Admin '{d['username']} existiert bereits. Eintrag wird übersprungen.")
else: else:
raise ValueError(f"Ungültige Daten für User Entry {d}") raise ValueError(f"Ungültige Daten für User Entry {d}")
else: else:
@ -417,7 +357,7 @@ def create_application(config):
except Exception as e: except Exception as e:
flash(f"Die Datei konnte nicht gelesen werden. Exception: {e}") flash(f"Die Datei konnte nicht gelesen werden. Exception: {e}")
return redirect('/manage_admins') return redirect('/manage_admins')
flash("Benutzer aus Datei gelesen.") flash("Admin Benutzer aus Datei gelesen.")
db.session.commit() db.session.commit()
else: else:
flash("Ungültige Dateiendung") flash("Ungültige Dateiendung")
@ -429,7 +369,7 @@ def create_application(config):
# token overview # token overview
@app.route('/tokens') @app.route('/tokens')
@roles_required('admin') @auth_required()
def list_tokens(): def list_tokens():
tokens = door.get_tokens() tokens = door.get_tokens()
assigned_tokens = {t: data for t, data in tokens.items() if not data['inactive']} assigned_tokens = {t: data for t, data in tokens.items() if not data['inactive']}
@ -438,7 +378,7 @@ def create_application(config):
# routes for registering, editing and deleting tokens # routes for registering, editing and deleting tokens
@app.route('/register-token', methods=['GET', 'POST']) @app.route('/register-token', methods=['GET', 'POST'])
@roles_required('admin') @auth_required()
def register(): def register():
"""Register new token for locking and unlocking the door. """Register new token for locking and unlocking the door.
@ -470,7 +410,7 @@ def create_application(config):
return render_template('register.html', token=door.get_most_recent_token(), form=form) return render_template('register.html', token=door.get_most_recent_token(), form=form)
@app.route('/edit-token/<token>', methods=['GET', 'POST']) @app.route('/edit-token/<token>', methods=['GET', 'POST'])
@roles_required('admin') @auth_required()
def edit_token(token): def edit_token(token):
"""Edit data in the token file (name, email, valid_thru date, active/inactive). """Edit data in the token file (name, email, valid_thru date, active/inactive).
@ -525,7 +465,7 @@ def create_application(config):
return render_template('edit.html', token=token, form=form) return render_template('edit.html', token=token, form=form)
@app.route('/store-token') @app.route('/store-token')
@roles_required('admin') @auth_required()
def store_token(): def store_token():
"""Store token to the token file on disk. """Store token to the token file on disk.
@ -547,7 +487,7 @@ def create_application(config):
return redirect('/tokens') return redirect('/tokens')
@app.route('/delete-token/<token>', methods=['GET', 'POST']) @app.route('/delete-token/<token>', methods=['GET', 'POST'])
@roles_required('admin') @auth_required()
def delete_token(token): def delete_token(token):
"""Delete the given token from the token file and store the new token file to disk """Delete the given token from the token file and store the new token file to disk
@ -588,7 +528,7 @@ def create_application(config):
return redirect('/tokens') return redirect('/tokens')
@app.route('/deactivate-token/<token>') @app.route('/deactivate-token/<token>')
@roles_required('admin') @auth_required()
def deactivate_token(token): def deactivate_token(token):
"""Deactivate access for the given token. This updates the token file on disk. """Deactivate access for the given token. This updates the token file on disk.
@ -608,7 +548,7 @@ def create_application(config):
return redirect('/tokens') return redirect('/tokens')
@app.route('/backup_tokens') @app.route('/backup_tokens')
@roles_required('admin') @auth_required()
def backup_tokens(): def backup_tokens():
# get list of defined admin users for backup # get list of defined admin users for backup
tokens = door.get_tokens() tokens = door.get_tokens()