From 337602c38833b6de4b16c9c525815560f9f181f9 Mon Sep 17 00:00:00 2001 From: Stefan Bethke Date: Fri, 30 May 2025 12:47:44 +0200 Subject: [PATCH] Pretty up --- hmdooris/BottleHelpers.py | 86 ++++++++++--- hmdooris/__main__.py | 4 +- hmdooris/static/icons.svg | 1 + hmdooris/static/main.css | 52 ++++++-- hmdooris/static/main.js | 65 +++++----- hmdooris/static/style.css | 147 ++++++++++++++++++++++ hmdooris/templates/base.html.j2 | 13 ++ hmdooris/templates/home.html.j2 | 22 ++-- hmdooris/templates/not_authorized.html.j2 | 21 ++-- hmdooris/templates/operate.html.j2 | 22 ++-- 10 files changed, 330 insertions(+), 103 deletions(-) create mode 100644 hmdooris/static/icons.svg create mode 100644 hmdooris/static/style.css create mode 100644 hmdooris/templates/base.html.j2 diff --git a/hmdooris/BottleHelpers.py b/hmdooris/BottleHelpers.py index b28fe10..1886452 100644 --- a/hmdooris/BottleHelpers.py +++ b/hmdooris/BottleHelpers.py @@ -14,7 +14,24 @@ class BottleHelpers: self.auth = auth self.group = group + def test_attrs(self, challenge, standard): + """Compare list or val the standard.""" + + stand_list = standard if type(standard) is list else [standard] + chal_list = challenge if type(challenge) is list else [challenge] + + for chal in chal_list: + if chal in stand_list: + return True + 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: @@ -23,23 +40,10 @@ class BottleHelpers: def require_attribute(self, attr, value): """ Decorator requires specific attribute value. """ - def test_attrs(challenge, standard): - """Compare list or val the standard.""" - - stand_list = standard if type(standard) is list else [standard] - chal_list = challenge if type(challenge) is list else [challenge] - - for chal in chal_list: - if chal in stand_list: - return True - return False - def _outer_wrapper(f): def _wrapper(*args, **kwargs): - if attr in self.auth.my_attrs: - resource = request.session[self.auth.sess_attr][attr] - if test_attrs(resource, value): - return f(*args, **kwargs) + if self.has_attribute(attr, value): + return f(*args, **kwargs) abort(401, 'Not Authorized: Not In Group') _wrapper.__name__ = f.__name__ @@ -48,12 +52,17 @@ class BottleHelpers: return _outer_wrapper 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: return self.require_attribute('groups', self.group)(func) else: def _outer_wrapper(f): def _wrapper(*args, **kwargs): - if self.auth.my_username is not None: + if self.is_logged_in(): return f(*args, **kwargs) abort(401, 'Not Authorized: Not logged in') return None @@ -64,15 +73,18 @@ class BottleHelpers: return _outer_wrapper(func) 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: return func def _outer_wrapper(f): def _wrapper(*args, **kwargs): - addr = ip_network(request.remote_addr) - for allowed in self.allowed: - if addr.overlaps(allowed): - return f(*args, **kwargs) + if self.is_authorized_ip(): + return f(*args, **kwargs) abort(401, 'Not Authorized: Wrong IP') return None @@ -80,3 +92,37 @@ class BottleHelpers: return _wrapper 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() diff --git a/hmdooris/__main__.py b/hmdooris/__main__.py index d3a8789..d073256 100644 --- a/hmdooris/__main__.py +++ b/hmdooris/__main__.py @@ -77,7 +77,9 @@ def websocket_endpoint(ws: WebSocket): @app.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/') diff --git a/hmdooris/static/icons.svg b/hmdooris/static/icons.svg new file mode 100644 index 0000000..82f472d --- /dev/null +++ b/hmdooris/static/icons.svg @@ -0,0 +1 @@ +arrow spin svgrepo commobile bolt svgrepo comunlock alt svgrepo comlock alt svgrepo com \ No newline at end of file diff --git a/hmdooris/static/main.css b/hmdooris/static/main.css index 3771680..b7c98e1 100644 --- a/hmdooris/static/main.css +++ b/hmdooris/static/main.css @@ -2,28 +2,66 @@ * foo */ -.lock-line { +body { + font-family: Arial, Helvetica, sans-serif; +} + +#locks { + width: 40em; +} + +.lock__line { display: flex; background: #ddd; padding: 20px 10px; } -.lock-name { - min-width: 20em; +.lock__line * { + 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; margin: 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; color: white; - border-radius: 1em; + min-width: 1.5em; + min-height: 1.5em; + border-radius: 1.5em; padding: 0.5em; - margin: 0 1em; + margin: 0 1em 0 0; font-size: 1.2em; 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; } \ No newline at end of file diff --git a/hmdooris/static/main.js b/hmdooris/static/main.js index 1bdc69f..2360f12 100644 --- a/hmdooris/static/main.js +++ b/hmdooris/static/main.js @@ -1,18 +1,22 @@ (function () { 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_unlock = document.getElementById(`lock__unlock__${lock.id}`); if (lock.status === "LOCKED") { - state.innerHTML = "🔒"; + state_icon.innerHTML = ''; + state_label.innerText = 'locked'; button_lock.disabled = true; button_unlock.disabled = false; } else if (lock.status === "UNLOCKED") { - state.innerHTML = "🔓"; + state_icon.innerHTML = ''; + state_label.innerText = 'unlocked'; button_lock.disabled = false; button_unlock.disabled = true; } else { - state.innerText = "?" + state_icon.innerHTML = ''; + state_label.innerText = 'unknown'; button_lock.disabled = true; button_unlock.disabled = true; } @@ -55,42 +59,37 @@ }); } - connect(); 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) { - let div = document.createElement("div"); - buttons.appendChild(div); - div.id = `lock__line__${lock.id}`; - div.classList.add("lock-line"); - let e = document.createElement("div"); - 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); + let lock_line = document.createElement("div"); + lock_line.id = `lock__line__${lock.id}`; + lock_line.classList.add("lock__line"); + lock_line.innerHTML = `
${lock.name}: 
`; + locks.appendChild(lock_line); - e = document.createElement("button"); - e.name = lock.id; - e.id = `lock__lock__${lock.id}`; - e.disabled = true; - e.innerText = "Lock"; - lock_button_add_event_handler(e, lock.id, true); - div.appendChild(e); + let lock_button = document.createElement("button"); + lock_button.name = lock.id; + lock_button.id = `lock__lock__${lock.id}`; + lock_button.disabled = true; + lock_button.innerText = "Lock"; + lock_button_add_event_handler(lock_button, lock.id, true); + 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); } + connect(); }).catch(error => console.error('Error:', error)); console.log("Setup complete") diff --git a/hmdooris/static/style.css b/hmdooris/static/style.css new file mode 100644 index 0000000..22f7088 --- /dev/null +++ b/hmdooris/static/style.css @@ -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); + } +} \ No newline at end of file diff --git a/hmdooris/templates/base.html.j2 b/hmdooris/templates/base.html.j2 new file mode 100644 index 0000000..50b79fb --- /dev/null +++ b/hmdooris/templates/base.html.j2 @@ -0,0 +1,13 @@ + + + + {% block page_title %}{% endblock %} + + + + + +

{{ self.page_title() }}

+{% block page_body %}{% endblock %} + + \ No newline at end of file diff --git a/hmdooris/templates/home.html.j2 b/hmdooris/templates/home.html.j2 index 9ee6deb..ea06382 100644 --- a/hmdooris/templates/home.html.j2 +++ b/hmdooris/templates/home.html.j2 @@ -1,15 +1,7 @@ - - - - HM Dooris - - -{# #} - - -

HM Dooris Status

-

Shows the status of the logs

-
-Lock or unlock a door - - \ No newline at end of file +{% extends "base.html.j2" %} +{% block page_title %}CCCHH Dooris{% endblock %} +{% block page_body %} +

Shows the status of the logs

+
+ Lock or unlock a door +{% endblock %} diff --git a/hmdooris/templates/not_authorized.html.j2 b/hmdooris/templates/not_authorized.html.j2 index 7f8911a..9516932 100644 --- a/hmdooris/templates/not_authorized.html.j2 +++ b/hmdooris/templates/not_authorized.html.j2 @@ -1,14 +1,7 @@ - - - - HM Dooris - {{ msg }} - - - - -

HM Dooris - {{ msg }}

-

You are not authorized to lock or unlock.

-

user: {{ user }}, groups: {{ groups }}, ip: {{ ip }}, code: {{ code }}, msg: {{ msg }}

-

{{ headers }}

- - \ No newline at end of file +{% extends "base.html.j2" %} +{% block page_title %}CCCHH Dooris - {{ msg }}{% endblock %} +{% block page_body %} +

You are not authorized to lock or unlock.

+

To be allowed to lock or unlock a door, you need to be on the correct network, and be in the Intern group.

+

user: {{ user }}, groups: {{ groups }}, ip: {{ ip }}, code: {{ code }}, msg: {{ msg }}

+{% endblock %} diff --git a/hmdooris/templates/operate.html.j2 b/hmdooris/templates/operate.html.j2 index 07e77c8..4d3354d 100644 --- a/hmdooris/templates/operate.html.j2 +++ b/hmdooris/templates/operate.html.j2 @@ -1,13 +1,9 @@ - - - - HM Dooris - - - - - -

HM Dooris

-
- - \ No newline at end of file +{% extends "base.html.j2" %} +{% block page_title %}CCCHH Dooris{% endblock %} +{% block page_body %} +
+
+

Click the respective buttons to lock or unlock a door. If the status of a door is ”unknown”, you will need to + run the lock manually by pressing the lock or unlock button on the door lock.

+
+{% endblock %}