Pretty up
All checks were successful
docker-image / docker (push) Successful in 1m24s

This commit is contained in:
Stefan Bethke 2025-05-30 12:47:44 +02:00
commit 337602c388
10 changed files with 330 additions and 103 deletions

View file

@ -14,16 +14,7 @@ class BottleHelpers:
self.auth = auth self.auth = auth
self.group = group self.group = group
def require_login(self, func: Callable) -> Callable: def test_attrs(self, challenge, standard):
if self.group is not None:
return self.auth.require_login(self.require_attribute('groups', self.group)(func))
else:
return self.auth.require_login(func)
def require_attribute(self, attr, value):
""" Decorator requires specific attribute value. """
def test_attrs(challenge, standard):
"""Compare list or val the standard.""" """Compare list or val the standard."""
stand_list = standard if type(standard) is list else [standard] stand_list = standard if type(standard) is list else [standard]
@ -34,11 +25,24 @@ class BottleHelpers:
return True return True
return False return False
def require_login(self, func: Callable) -> Callable:
"""
Check if user is logged in and redirect to OIDC provider if not.
If a group has been defined, check the group membership and abort with a 401 if the user is not a member.
:param func: decorator target
:return: decorated function
"""
if self.group is not None:
return self.auth.require_login(self.require_attribute('groups', self.group)(func))
else:
return self.auth.require_login(func)
def require_attribute(self, attr, value):
""" Decorator requires specific attribute value. """
def _outer_wrapper(f): def _outer_wrapper(f):
def _wrapper(*args, **kwargs): def _wrapper(*args, **kwargs):
if attr in self.auth.my_attrs: if self.has_attribute(attr, value):
resource = request.session[self.auth.sess_attr][attr]
if test_attrs(resource, value):
return f(*args, **kwargs) return f(*args, **kwargs)
abort(401, 'Not Authorized: Not In Group') abort(401, 'Not Authorized: Not In Group')
@ -48,12 +52,17 @@ class BottleHelpers:
return _outer_wrapper return _outer_wrapper
def require_authz(self, func: Callable) -> Callable: def require_authz(self, func: Callable) -> Callable:
"""
If a group has been defined, check the group membership; else check if user is logged in. Abort with 401 if not.
:param func: decorator target
:return: decorated function
"""
if self.group is not None: if self.group is not None:
return self.require_attribute('groups', self.group)(func) return self.require_attribute('groups', self.group)(func)
else: else:
def _outer_wrapper(f): def _outer_wrapper(f):
def _wrapper(*args, **kwargs): def _wrapper(*args, **kwargs):
if self.auth.my_username is not None: if self.is_logged_in():
return f(*args, **kwargs) return f(*args, **kwargs)
abort(401, 'Not Authorized: Not logged in') abort(401, 'Not Authorized: Not logged in')
return None return None
@ -64,14 +73,17 @@ class BottleHelpers:
return _outer_wrapper(func) return _outer_wrapper(func)
def require_sourceip(self, func: Callable) -> Callable: def require_sourceip(self, func: Callable) -> Callable:
"""
Check that the user is coming from an allowed IP address. Abort with 401 if not.
:param func: decorator target
:return: decorated function
"""
if self.allowed is None or len(self.allowed) == 0: if self.allowed is None or len(self.allowed) == 0:
return func return func
def _outer_wrapper(f): def _outer_wrapper(f):
def _wrapper(*args, **kwargs): def _wrapper(*args, **kwargs):
addr = ip_network(request.remote_addr) if self.is_authorized_ip():
for allowed in self.allowed:
if addr.overlaps(allowed):
return f(*args, **kwargs) return f(*args, **kwargs)
abort(401, 'Not Authorized: Wrong IP') abort(401, 'Not Authorized: Wrong IP')
return None return None
@ -80,3 +92,37 @@ class BottleHelpers:
return _wrapper return _wrapper
return _outer_wrapper(func) return _outer_wrapper(func)
def has_attribute(self, attr, value):
if attr in self.auth.my_attrs:
resource = request.session[self.auth.sess_attr][attr]
return self.test_attrs(resource, value)
return False
def is_logged_in(self):
return self.auth.my_username is not None
def is_in_group(self):
"""
Returns True if user is in configured group, or no group has been configured.
:return: True if user is in configured group
"""
if self.group is not None:
return self.has_attribute('groups', self.group)
return True
def is_authorized_ip(self) -> bool:
if len(self.allowed) == 0:
return True
addr = ip_network(request.remote_addr)
for allowed in self.allowed:
if addr.overlaps(allowed):
return True
return False
def is_authorized(self) -> bool:
"""
Returns True if the user is coming from the right IP address, is logged in and belongs to the right group.
:return:
"""
return self.is_authorized_ip() and self.is_logged_in() and self.is_in_group()

View file

@ -77,7 +77,9 @@ def websocket_endpoint(ws: WebSocket):
@app.get('/api/lock') @app.get('/api/lock')
def get_api_lock(): def get_api_lock():
return update_poller.get_locks(True) data = update_poller.get_locks(True)
data['authorized'] = bottle_helpers.is_authorized()
return data
@app.get('/api/lock/<id>') @app.get('/api/lock/<id>')

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" style="height: 0; width: 0; position: absolute;"><symbol id="icon-arrow-spin-svgrepo-com" viewBox="0 0 24 24"><title>arrow spin svgrepo com</title><g class="nc-icon-wrapper"><path d="M12 20C9.47362 20 7.22075 18.8289 5.75463 17M12 4C14.9611 4 17.5465 5.60879 18.9297 8M4 12C4 9.47362 5.17107 7.22075 7 5.75463M20 12C20 14.8339 18.5265 17.3236 16.3039 18.7448M19.3 5V8H16.3M8 16.3H5V19.3M16.3 16V19H19.3M4.7002 5H7.7002V8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" fill="none"></path></g></symbol><symbol id="icon-mobile-bolt-svgrepo-com" viewBox="0 0 24 24"><title>mobile bolt svgrepo com</title><g class="nc-icon-wrapper"><path d="M12 9L10 12H14L12 15M9.2 21H14.8C15.9201 21 16.4802 21 16.908 20.782C17.2843 20.5903 17.5903 20.2843 17.782 19.908C18 19.4802 18 18.9201 18 17.8V6.2C18 5.0799 18 4.51984 17.782 4.09202C17.5903 3.71569 17.2843 3.40973 16.908 3.21799C16.4802 3 15.9201 3 14.8 3H9.2C8.0799 3 7.51984 3 7.09202 3.21799C6.71569 3.40973 6.40973 3.71569 6.21799 4.09202C6 4.51984 6 5.07989 6 6.2V17.8C6 18.9201 6 19.4802 6.21799 19.908C6.40973 20.2843 6.71569 20.5903 7.09202 20.782C7.51984 21 8.07989 21 9.2 21Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" fill="none"></path></g></symbol><symbol id="icon-unlock-alt-svgrepo-com" viewBox="0 0 24 24"><title>unlock alt svgrepo com</title><g class="nc-icon-wrapper"><path d="M16.584 6C15.8124 4.2341 14.0503 3 12 3C9.23858 3 7 5.23858 7 8V10.0288M12 14.5V16.5M7 10.0288C7.47142 10 8.05259 10 8.8 10H15.2C16.8802 10 17.7202 10 18.362 10.327C18.9265 10.6146 19.3854 11.0735 19.673 11.638C20 12.2798 20 13.1198 20 14.8V16.2C20 17.8802 20 18.7202 19.673 19.362C19.3854 19.9265 18.9265 20.3854 18.362 20.673C17.7202 21 16.8802 21 15.2 21H8.8C7.11984 21 6.27976 21 5.63803 20.673C5.07354 20.3854 4.6146 19.9265 4.32698 19.362C4 18.7202 4 17.8802 4 16.2V14.8C4 13.1198 4 12.2798 4.32698 11.638C4.6146 11.0735 5.07354 10.6146 5.63803 10.327C5.99429 10.1455 6.41168 10.0647 7 10.0288Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" fill="none"></path></g></symbol><symbol id="icon-lock-alt-svgrepo-com" viewBox="0 0 24 24"><title>lock alt svgrepo com</title><g class="nc-icon-wrapper"><path d="M12 14.5V16.5M7 10.0288C7.47142 10 8.05259 10 8.8 10H15.2C15.9474 10 16.5286 10 17 10.0288M7 10.0288C6.41168 10.0647 5.99429 10.1455 5.63803 10.327C5.07354 10.6146 4.6146 11.0735 4.32698 11.638C4 12.2798 4 13.1198 4 14.8V16.2C4 17.8802 4 18.7202 4.32698 19.362C4.6146 19.9265 5.07354 20.3854 5.63803 20.673C6.27976 21 7.11984 21 8.8 21H15.2C16.8802 21 17.7202 21 18.362 20.673C18.9265 20.3854 19.3854 19.9265 19.673 19.362C20 18.7202 20 17.8802 20 16.2V14.8C20 13.1198 20 12.2798 19.673 11.638C19.3854 11.0735 18.9265 10.6146 18.362 10.327C18.0057 10.1455 17.5883 10.0647 17 10.0288M7 10.0288V8C7 5.23858 9.23858 3 12 3C14.7614 3 17 5.23858 17 8V10.0288" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" fill="none"></path></g></symbol></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -2,28 +2,66 @@
* foo * foo
*/ */
.lock-line { body {
font-family: Arial, Helvetica, sans-serif;
}
#locks {
width: 40em;
}
.lock__line {
display: flex; display: flex;
background: #ddd; background: #ddd;
padding: 20px 10px; padding: 20px 10px;
} }
.lock-name { .lock__line * {
min-width: 20em; flex-grow: 1;
} }
.lock-line button { .lock__state-and-label {
flex-grow: 2;
}
.lock__name, .lock__state-label {
display: inline-block;
vertical-align: top;
margin: 1em 0 0 0;
}
.lock__line button {
min-width: 10em; min-width: 10em;
margin: 0.1em 0.5em; margin: 0.1em 0.5em;
padding: 0.1em 0.5em; padding: 0.1em 0.5em;
border-radius: 1em;
background-color: white;
color: black;
} }
.lock-line .lock-status { .lock__line .lock__state-icon {
display: inline-block;
background: black; background: black;
color: white; color: white;
border-radius: 1em; min-width: 1.5em;
min-height: 1.5em;
border-radius: 1.5em;
padding: 0.5em; padding: 0.5em;
margin: 0 1em; margin: 0 1em 0 0;
font-size: 1.2em; font-size: 1.2em;
font-weight: bold; font-weight: bold;
} }
svg.icon {
width: 32px;
height: 32px;
stroke-width: 2px;
margin-bottom: -.2em;
}
.unauthorized__debug {
color: #444;
font-size: 0.7em;
position: absolute;
bottom: 0;
}

View file

@ -1,18 +1,22 @@
(function () { (function () {
let update_button = function (lock) { let update_button = function (lock) {
let state = document.getElementById(`lock__state__${lock.id}`); let state_icon = document.getElementById(`lock__state-icon__${lock.id}`);
let state_label = document.getElementById(`lock__state-label__${lock.id}`);
let button_lock = document.getElementById(`lock__lock__${lock.id}`); let button_lock = document.getElementById(`lock__lock__${lock.id}`);
let button_unlock = document.getElementById(`lock__unlock__${lock.id}`); let button_unlock = document.getElementById(`lock__unlock__${lock.id}`);
if (lock.status === "LOCKED") { if (lock.status === "LOCKED") {
state.innerHTML = "&#x1F512"; state_icon.innerHTML = '<svg class="icon"><use href="static/icon-lock-alt-svgrepo-com"/></svg>';
state_label.innerText = 'locked';
button_lock.disabled = true; button_lock.disabled = true;
button_unlock.disabled = false; button_unlock.disabled = false;
} else if (lock.status === "UNLOCKED") { } else if (lock.status === "UNLOCKED") {
state.innerHTML = "&#x1F513"; state_icon.innerHTML = '<svg class="icon"><use href="static/icon-unlock-alt-svgrepo-com"/></svg>';
state_label.innerText = 'unlocked';
button_lock.disabled = false; button_lock.disabled = false;
button_unlock.disabled = true; button_unlock.disabled = true;
} else { } else {
state.innerText = "?" state_icon.innerHTML = '<svg class="icon"><use href="static/icons.svg#icon-arrow-spin-svgrepo-com"/></svg>';
state_label.innerText = 'unknown';
button_lock.disabled = true; button_lock.disabled = true;
button_unlock.disabled = true; button_unlock.disabled = true;
} }
@ -55,42 +59,37 @@
}); });
} }
connect();
fetch("/api/lock").then((res) => res.json()).then((data) => { fetch("/api/lock").then((res) => res.json()).then((data) => {
let buttons = document.getElementById("locks"); if (!data.authorized)
return;
let locks = document.getElementById("locks");
for (let lock of data.locks) { for (let lock of data.locks) {
let div = document.createElement("div"); let lock_line = document.createElement("div");
buttons.appendChild(div); lock_line.id = `lock__line__${lock.id}`;
div.id = `lock__line__${lock.id}`; lock_line.classList.add("lock__line");
div.classList.add("lock-line"); lock_line.innerHTML = `<div class="lock__state-and-label"><span id="lock__state-icon__${lock.id}" class="lock__state-icon"></span><span class="lock__name">${lock.name}:&nbsp;</span><span id="lock__state-label__${lock.id}" class="lock__state-label"></span></div>`;
let e = document.createElement("div"); locks.appendChild(lock_line);
e.innerText = `${lock.name} `;
e.classList.add("lock-name");
div.appendChild(e);
let state = document.createElement("span");
state.id = `lock__state__${lock.id}`;
state.classList.add("lock-status");
state.innerText = "?";
e.prepend(state);
e = document.createElement("button"); let lock_button = document.createElement("button");
e.name = lock.id; lock_button.name = lock.id;
e.id = `lock__lock__${lock.id}`; lock_button.id = `lock__lock__${lock.id}`;
e.disabled = true; lock_button.disabled = true;
e.innerText = "Lock"; lock_button.innerText = "Lock";
lock_button_add_event_handler(e, lock.id, true); lock_button_add_event_handler(lock_button, lock.id, true);
div.appendChild(e); lock_line.appendChild(lock_button);
lock_button = document.createElement("button");
lock_button.name = lock.id;
lock_button.id = `lock__unlock__${lock.id}`;
lock_button.disabled = true;
lock_button.innerText = "Unlock";
lock_button_add_event_handler(lock_button, lock.id, false);
lock_line.appendChild(lock_button);
e = document.createElement("button");
e.name = lock.id;
e.id = `lock__unlock__${lock.id}`;
e.disabled = true;
e.innerText = "Unlock";
lock_button_add_event_handler(e, lock.id, false);
div.appendChild(e);
update_button(lock); update_button(lock);
} }
connect();
}).catch(error => console.error('Error:', error)); }).catch(error => console.error('Error:', error));
console.log("Setup complete") console.log("Setup complete")

147
hmdooris/static/style.css Normal file
View file

@ -0,0 +1,147 @@
/* Generated using nucleoapp.com */
/* --------------------------------
General
-------------------------------- */
:root {
--icon-color-primary: inherit;
--icon-color-secondary: currentColor;
}
.icon {
display: inline-block;
color: var(--icon-color-primary); /* icon primary color */
height: 1em;
width: 1em;
line-height: 1;
flex-shrink: 0;
max-width: initial;
}
.icon use {
/* icon secondary color */
fill: var(--icon-color-secondary);
stroke: var(--icon-color-secondary);
}
/* --------------------------------
Themes
-------------------------------- */
.icon-theme-1 {
--icon-color-primary: #212121;
--icon-color-secondary: inherit;
}
/* --------------------------------
Sizes
-------------------------------- */
:root {
--icon-sm: 0.8em;
--icon-lg: 1.2em;
}
/* relative units */
.icon-sm {
font-size: var(--icon-sm);
}
.icon-lg {
font-size: var(--icon-lg);
}
/* absolute units */
.icon-16 {
font-size: 16px;
}
.icon-32 {
font-size: 32px;
}
/* --------------------------------
Stroke
-------------------------------- */
.stroke-1 {
stroke-width: 1px;
}
.stroke-2 {
stroke-width: 2px;
}
.stroke-3 {
stroke-width: 3px;
}
.stroke-4 {
stroke-width: 4px;
}
/* --------------------------------
Caps/Corners
-------------------------------- */
.icon use {
--icon-stroke-linecap-butt: butt;
stroke-miterlimit: 10;
stroke-linecap: square;
stroke-linejoin: miter;
}
.stroke-round use {
--icon-stroke-linecap-butt: round;
stroke-linecap: round;
stroke-linejoin: round;
}
/* --------------------------------
Transformations/Animations
-------------------------------- */
.icon-rotate-90 {
transform: rotate(90deg);
}
.icon-rotate-180 {
transform: rotate(180deg);
}
.icon-rotate-270 {
transform: rotate(270deg);
}
.icon-flip-y {
transform: scaleY(-1);
}
.icon-flip-x {
transform: scaleX(-1);
}
.icon-is-spinning {
animation: icon-spin 1s infinite linear;
}
@keyframes icon-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block page_title %}{% endblock %}</title>
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel=stylesheet type="text/css" href="static/main.css">
<script src="static/main.js" defer></script>
</head>
<body>
<h1>{{ self.page_title() }}</h1>
{% block page_body %}{% endblock %}
</body>
</html>

View file

@ -1,15 +1,7 @@
<!DOCTYPE html> {% extends "base.html.j2" %}
<html lang="en"> {% block page_title %}CCCHH Dooris{% endblock %}
<head> {% block page_body %}
<title>HM Dooris</title> <p>Shows the status of the logs</p>
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"> <div id="locks"></div>
<link rel=stylesheet type="text/css" href="static/main.css"> <a href="operate">Lock or unlock a door</a>
{# <script src="static/main.js" defer></script>#} {% endblock %}
</head>
<body>
<h1>HM Dooris Status</h1>
<p>Shows the status of the logs</p>
<div id="locks"></div>
<a href="operate">Lock or unlock a door</a>
</body>
</html>

View file

@ -1,14 +1,7 @@
<!DOCTYPE html> {% extends "base.html.j2" %}
<html lang="en"> {% block page_title %}CCCHH Dooris - {{ msg }}{% endblock %}
<head> {% block page_body %}
<title>HM Dooris - {{ msg }}</title> <p>You are not authorized to lock or unlock.</p>
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"> <p>To be allowed to lock or unlock a door, you need to be on the correct network, and be in the Intern group.</p>
<link rel=stylesheet type="text/css" href="static/main.css"> <p class="unauthorized__debug">user: {{ user }}, groups: {{ groups }}, ip: {{ ip }}, code: {{ code }}, msg: {{ msg }}</p>
</head> {% endblock %}
<body>
<h1>HM Dooris - {{ msg }}</h1>
<p>You are not authorized to lock or unlock.</p>
<p>user: {{ user }}, groups: {{ groups }}, ip: {{ ip }}, code: {{ code }}, msg: {{ msg }}</p>
<p>{{ headers }}</p>
</body>
</html>

View file

@ -1,13 +1,9 @@
<!DOCTYPE html> {% extends "base.html.j2" %}
<html lang="en"> {% block page_title %}CCCHH Dooris{% endblock %}
<head> {% block page_body %}
<title>HM Dooris</title> <div id="locks"></div>
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"> <div>
<link rel=stylesheet type="text/css" href="static/main.css"> <p>Click the respective buttons to lock or unlock a door. If the status of a door is ”unknown”, you will need to
<script src="static/main.js" defer></script> run the lock manually by pressing the lock or unlock button on the door lock.</p>
</head> </div>
<body> {% endblock %}
<h1>HM Dooris</h1>
<div id="locks"></div>
</body>
</html>