From b0f19eb72ea5a5edce0cc0b842cf8fbb4adbfb32 Mon Sep 17 00:00:00 2001 From: chlub Date: Mon, 15 Dec 2025 23:07:19 +0100 Subject: [PATCH] user handling --- components.py | 1 - db_connect.py | 35 +++- mqtt_client.py | 1 + sql_startup.sql | 23 ++- static/scripts.js | 8 +- static/styles.css | 362 +++++++++++++++++++++++++++++++------- templating.py | 30 +++- user_handling.py | 14 +- web.py | 105 ++++++++--- web/base.html.jinja | 10 +- web/empty_room.html.jinja | 8 + web/index.html.jinja | 11 +- web/landing.html.jinja | 9 +- web/room.html.jinja | 2 +- web/users.html.jinja | 81 +++++++++ 15 files changed, 573 insertions(+), 127 deletions(-) create mode 100644 web/empty_room.html.jinja create mode 100644 web/users.html.jinja diff --git a/components.py b/components.py index cdc9b6d..6926f78 100644 --- a/components.py +++ b/components.py @@ -12,7 +12,6 @@ class AnonymousUser(AnonymousUserMixin): self.id = 1 self.username = "nouser" -UserInfo = namedtuple('UserInfo',['id', 'name']) RoomInfo = namedtuple('RoomInfo',['name', 'shortcode', 'roomdata']) # state is an int not in db # 0 - value valid diff --git a/db_connect.py b/db_connect.py index c619be4..b021388 100644 --- a/db_connect.py +++ b/db_connect.py @@ -38,26 +38,40 @@ class DatabaseConnect: case 3: logging.info(f"Log emitted: {message}") - def create_user(self, username: str, salt: bytes, key: bytes) -> None: - self.cursor.execute("INSERT INTO `users` (`username`, `salt`, `key`) VALUES (?,?,?)",(username, salt, key)) + def create_user(self, username: str, salt: bytes, key: bytes, administer: bool = False) -> None: + self.cursor.execute("INSERT INTO `users` (`username`, `salt`, `key`, `administer`) VALUES (?,?,?,?)",(username, salt, key, administer)) self.conn.commit() self.emit_log(2, f"Created user {username}") + def change_user_password(self, id: int, salt: bytes, key: bytes) -> None: + self.cursor.execute("UPDATE `users` SET `salt` = ?, `key` = ? WHERE `ID` = ?;",(salt, key, id)) + self.conn.commit() + self.emit_log(2, f"Changed password for user ID {id}") + def delete_user(self, id) -> None: - self.cursor.execute("DELETE FROM Users WHERE ID = ?",(id)) + self.cursor.execute("DELETE FROM Users WHERE ID = ?",(id,)) + self.conn.commit() self.emit_log(2, f"Deleted user ID {id}") - def display_users(self) -> tuple[tuple[int, str, int]]: - self.cursor.execute("SELECT * FROM Users") + def display_users(self) -> tuple[tuple[int, str, bool]]: + self.cursor.execute("SELECT users.`ID`, users.`username`, users.`administer` FROM Users") return self.cursor.fetchall() def username_from_id(self, user_id): self.cursor.execute("SELECT users.`username` FROM users WHERE `ID` = ?;", (user_id,)) return self.cursor.fetchone() - def user_by_username(self, username) -> tuple[int, str, bytes, bytes] | None: + def user_by_username(self, username) -> tuple[int, str, bytes, bytes, bool] | None: self.cursor.execute("SELECT * FROM users WHERE `username` = ?;", (username,)) return self.cursor.fetchone() + + def is_admin(self, user_id) -> bool: + self.cursor.execute("SELECT users.`administer` FROM users WHERE `ID` = ?;", (user_id,)) + res = self.cursor.fetchone() + if res is None: + return False + else: + return res[0] == 1 def view_valid_rooms(self, user_id) -> list[tuple[str, str, int]]: self.cursor.execute( @@ -65,6 +79,11 @@ class DatabaseConnect: (user_id,)) return self.cursor.fetchall() + def view_all_rooms(self) -> list[tuple[str, str, int]]: + self.cursor.execute( + "SELECT rooms.name, rooms.shortname, rooms.ID from rooms;") + return self.cursor.fetchall() + def user_has_room_perms(self, user_id, room_shortname) -> bool: self.cursor.execute("SELECT permissions.`view` FROM permissions LEFT JOIN rooms ON permissions.`roomID` = rooms.ID WHERE rooms.shortname = ? AND `userID` = ?;",(room_shortname, user_id)) res = self.cursor.fetchone() @@ -78,11 +97,11 @@ class DatabaseConnect: # return self.cursor.fetchone() def get_sensors_in_room_shortname(self, shortname) -> list[tuple[int, int]]: - self.cursor.execute("SELECT Sensors.`ID`, Sensors.`Type` from Sensors LEFT JOIN rooms ON Sensors.`roomID` = rooms.ID WHERE rooms.shortname = ?;",(shortname,)) + self.cursor.execute("Select sensors.`ID`, sensors.`Type` FROM sensors LEFT JOIN devices ON devices.`ID` = sensors.`deviceID` LEFT JOIN rooms ON devices.`roomID` = rooms.`ID` WHERE rooms.shortname = ?;",(shortname,)) return [x for x in self.cursor.fetchall()] def get_sensors_in_room_id(self, roomid) -> list[tuple[int, int]]: - self.cursor.execute("SELECT Sensors.`ID`, Sensors.`Type` from Sensors LEFT JOIN rooms ON Sensors.`roomID` = rooms.ID WHERE rooms.`ID` = ?;",(roomid,)) + self.cursor.execute("Select sensors.`ID`, sensors.`Type` FROM sensors LEFT JOIN devices ON devices.`ID` = sensors.`deviceID` LEFT JOIN rooms ON devices.`roomID` = rooms.`ID` WHERE rooms.`ID` = ?;",(roomid,)) return [(x[0], x[1]) for x in self.cursor.fetchall()] def get_sensorsinfo_by_sensorid(self, sensorid) -> SensorInfo | None: diff --git a/mqtt_client.py b/mqtt_client.py index 1f6c8a4..0050147 100644 --- a/mqtt_client.py +++ b/mqtt_client.py @@ -43,6 +43,7 @@ def on_message(client: MQTTClient, topic: str, payload: bytes, qos, properties): db_connect.DatabaseConnect().create_sensor_reading(id, datetime.datetime.fromisoformat(data[0]), float(data[1])) else: db_connect.DatabaseConnect().emit_log(1, f"Received MQTT message for unknown topic {topic}") + #TODO: add device topics and handle those commands @fast_mqtt.on_disconnect() def on_disconnect(client: MQTTClient, packet, exc=None): diff --git a/sql_startup.sql b/sql_startup.sql index b6056f9..415005c 100644 --- a/sql_startup.sql +++ b/sql_startup.sql @@ -4,23 +4,32 @@ GRANT ALL PRIVILEGES ON sensors.* TO SensorsAdmin; USE sensors; GO -CREATE TABLE Users (`ID` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `username` TEXT, `pwd` INT); -INSERT INTO Users (`ID`, `username`, `pwd`) VALUES (1,'nouser',NULL), (NULL,'Admin',1); +CREATE TABLE Users (`ID` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `username` TEXT, `salt` BLOB, `key` BLOB, `administer` BOOLEAN DEFAULT 0); +INSERT INTO Users (`ID`, `username`, `salt`, `key`, `administer`) VALUES (1,'nouser',NULL,NULL,0); -CREATE TABLE Permissions (`userID` INT, `roomID` INT, `view` BOOLEAN DEFAULT 0, `purge_data` BOOLEAN DEFAULT 0, `administer` BOOLEAN DEFAULT 0); +CREATE TABLE Permissions (`ID` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `userID` INT, `roomID` INT, `view` BOOLEAN DEFAULT 0, `purge_data` BOOLEAN DEFAULT 0, `administer` BOOLEAN DEFAULT 0); CREATE TABLE Rooms (`ID` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `name` TEXT, `shortname` TEXT UNIQUE); -CREATE TABLE Sensors(`ID` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `type` INT, `roomID` INT, `mqttTopic` TEXT); +CREATE TABLE Sensors(`ID` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `type` INT, `deviceID` INT, `mqttTopic` TEXT, `name` TEXT); CREATE TABLE Types(`ID` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `type_desc` TEXT); -CREATE TABLE Devices(`ID` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `roomID` INT, `name` TEXT, `description` TEXT); +CREATE TABLE Devices(`ID` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `roomID` INT, `name` TEXT, `description` TEXT, `mqttTopic` TEXT); +CREATE TABLE DeviceLog( `timestamp` TIMESTAMP, `deviceID` INT, `message` TEXT); CREATE TABLE Readings(`sensorID` INT, `Timestamp` DATETIME, `reading` DOUBLE); CREATE INDEX `sensor_index` ON Readings (`sensorID`); CREATE TABLE Log( `timestamp` TIMESTAMP, `type` INT, `message` TEXT); -INSERT INTO Rooms (`name`,`shortname`) VALUES ('101','101'),('102','102'),('210','210'),('211','211'),('215 - Studovna','215'); -INSERT INTO Sensors (`type`, `roomID`,`mqttTopic`) VALUES (0,1,"101/floortemp"),(0,1,"101/ceiltemp"),(1,1,"101/humidity"),(0,3,"210/temp"),(0,5,"215/temp"),(1,5,"215/humidity"); +INSERT INTO Rooms (`name`,`shortname`) VALUES ('101','101'),('102','102'),('210','210'),('211','211'),('215 - Studovna','215')('000','Venku'); + +INSERT INTO Devices (`roomID`, `name`, `description`, `mqttTopic`) VALUES + (1, "Raspberry Pi 101", "Main controller for room 101", "101/device"), + (3, "Raspberry Pi 210", "Main controller for room 210", "210/device"), + (5, "Raspberry Pi 215", "Main controller for room 215", "215/device"), + (6, "Outdoor Sensor", "Outdoor temperature sensor", "000/device"); + +INSERT INTO Sensors (`type`, `deviceID`,`mqttTopic`,`name`) VALUES (0,1,"101/floortemp", "Floor Temperatue"),(0,1,"101/ceiltemp", "Ceiling Temperature"),(1,1,"101/humidity", "Room Humidity"), + (0,3,"210/temp", "Whole Room Temperature"),(0,5,"215/temp", "Whole Room Temperature"),(1,5,"215/humidity", "Whole Room Humidity"),(0,4,"000/temp", "Outdoor Temperature"); INSERT INTO Types (type_desc) VALUES ("Temperature"), ('Humidity'); INSERT INTO `readings`(`Timestamp`,`reading`,`sensorID`) VALUES ('2025-11-12 00:00:00',25.7,1),('2025-11-12 01:00:00',26.7,1),('2025-11-12 02:00:00',27.4,1),('2025-11-12 02:04:00',28.0,1),('2025-11-12 03:22:00',28.2,1), diff --git a/static/scripts.js b/static/scripts.js index b6d65b8..db856aa 100644 --- a/static/scripts.js +++ b/static/scripts.js @@ -1,11 +1,11 @@ -function tableSearch(){ +function tableSearch(own_elem, tableID, rowsearch=0) { var table, input, tr, td, row_content; - table = document.getElementById("tableRooms"); - input = document.getElementById("tableRoomSearch").value.toUpperCase(); + table = document.getElementById(tableID); + input = document.getElementById(own_elem).value.toUpperCase(); tr = table.getElementsByTagName("tr"); for (let i = 0; i < tr.length; i++) { - td = tr[i].getElementsByTagName("td")[0]; + td = tr[i].getElementsByTagName("td")[rowsearch]; if(td){ row_content = td.textContent || td.innerText; if (row_content.toUpperCase().indexOf(input) > -1) { diff --git a/static/styles.css b/static/styles.css index 99eecda..d0ff4b7 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1,98 +1,326 @@ -*{ - font-family: sans-serif; +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; } -a{ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #8e8e8e 0%, #1f1f1f 100%); + color: #333; + min-height: 100vh; + padding: 20px; + font-size: 1.3rem; + line-height: 1.5; +} + +:root { + --primary-color: #202a55; + --secondary-color: #5439ac; + --accent-color: #f093fb; + --text-color: #333; + --light-bg: #c4c4c4; + --dark-bg: #343a40; + --border-color: #dee2e6; + --shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + --border-radius: 8px; + --transition: all 0.3s ease; +} + +a { + color: var(--secondary-color); text-decoration: none; + transition: var(--transition); + font-weight: bold; } -.main-flex{ +a:hover { + color: var(--accent-color); + text-decoration: underline; +} + +.main-flex { display: flex; flex-direction: row; + background: white; + border-radius: var(--border-radius); + box-shadow: var(--shadow); + overflow: hidden; + min-height: calc(100vh - 40px); } -.navbar{ - flex: 1 10vw; - white-space: nowrap; -} - -.topbranding, .navbar-inner, .topline{ - font-weight: bold; - border: solid 0.2em; - padding-left: 0.5em; - padding-right: 0.5em; - padding-bottom: 0.25em; - padding-top: 0.25em; -} - -.navbar-inner{ - font-weight: normal; -} - -.right-flex{ - flex: 1 60vw; -} - -.topline{ +.navbar { + flex: 0 0 250px; + background: var(--dark-bg); + color: white; + padding: 20px; display: flex; + flex-direction: column; + flex-grow: 1; } -.section{ + +.topbranding { + font-size: 1.8rem; + font-weight: bold; + margin-bottom: 20px; + padding: 10px; + background: var(--primary-color); + border-radius: var(--border-radius); + text-align: center; +} + +.navbar-inner { flex: 1; } -.logout{ - text-align: right; + +.navbar-inner a { + color: white; + display: block; + padding: 10px 0; + border-radius: var(--border-radius); + margin-bottom: 5px; } -.content{ - padding-left: 1em; +.navbar-inner a:hover { + background: var(--primary-color); } -.rooms-table{ - background-color: lightgray; - padding: 0.5em 1.5em; - width: min-content; +.navbar-inner ul { + list-style: none; + padding-left: 20px; } -.rooms-table table{ - width: 50vw; - background-color: inherit; - text-align: center; - border-color: darkgray; - border-collapse: collapse; - border-left: 10px; +.navbar-inner li { + margin-bottom: 5px; } -.rooms-table tr{ - padding-left: 1em; + +.navbar-inner li a { + padding: 5px 0; } -.rooms-table td{ - padding-bottom: 0.2em; - padding-top: 0.2em; + +.right-flex { + flex: 1; + padding: 20px; + display: flex; + flex-direction: column; + flex-grow: 9; } -thead{ - border-bottom: 0.25em darkgray solid; - + +.topline { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 10px; + border-bottom: 1px solid var(--border-color); } -tbody tr{ - border-bottom: 0.1em darkgray solid; + +.intro-text{ + margin-bottom: 20px; } -.data-missing{ - background-color: gray; - color: transparent -} -.data-late{ - background-color: orange; -} -.room-table-elem, .searchbar{ + +.user { font-weight: bold; - text-align: left; } -.rooms-table input{ - width: 90%; +.logout a { + color: var(--primary-color); + font-weight: bold; +} + +.content { + flex: 1; + padding: 20px 0; +} + +.rooms-table { + background: var(--light-bg); + border-radius: var(--border-radius); + padding: 20px; + box-shadow: var(--shadow); + overflow-x: auto; +} + +table { + width: 100%; + background: white; + border-collapse: collapse; + border-radius: var(--border-radius); + overflow: hidden; + box-shadow: var(--shadow); +} + +th, td { + padding: 15px; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +th { + background: var(--primary-color); + color: white; + font-weight: bold; +} + +tbody tr:hover { + background: #f1f3f4; +} + +table input { + width: 100%; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + font-size: 1rem; +} + +.searchbar { + background: var(--light-bg) !important; + border-bottom: none !important; +} + +.searchbar td { + padding: 10px 15px; +} + +.data-missing { + background: #6c757d; + color: white; +} + +.data-late { + background: #ffc107; + color: #212529; +} + +.data-valid { + background: #28a745; + color: white; } .current-readings .data-valid, .current-readings .data-late, -.current-readings .data-missing{ - padding: 0.2em 1em; -} \ No newline at end of file +.current-readings .data-missing { + padding: 10px 15px; + border-radius: var(--border-radius); + margin: 5px; + display: inline-block; +} + +.content h2 { + color: var(--primary-color); + margin-bottom: 20px; +} + +form { + max-width: 400px; + margin: 0 auto; +} + +form label { + display: block; + margin-bottom: 5px; + font-weight: bold; + color: var(--text-color); +} + +form input { + width: 100%; + padding: 10px; + margin-bottom: 15px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + font-size: 1rem; + box-sizing: border-box; +} + +form button, +table button { + width: 100%; + padding: 12px; + background: var(--primary-color); + color: white; + border: none; + border-radius: var(--border-radius); + font-size: 1rem; + font-weight: bold; + cursor: pointer; + transition: var(--transition); +} + +form button:hover, +table button:hover { + background: var(--secondary-color); +} + +.fake-button { + width: 100%; + background: gray; + color: white; + padding: 10px 15px; + margin: 0 auto; + border-radius: var(--border-radius); + text-align: center; + font-size: 1rem; + font-weight: bold; + max-width: 400px; +} + +.flashes { + list-style: none; + padding: 0; + margin-top: 20px; +} + +.flashes li { + padding: 10px; + margin-bottom: 10px; + border-radius: var(--border-radius); + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +/* Room Page Styles */ +.current-readings { + margin-top: 20px; +} + +.current-readings h3 { + color: var(--primary-color); + margin-bottom: 20px; +} + +.current-readings > div > div { + border: 2px solid var(--border-color); + border-radius: var(--border-radius); + width: fit-content; + text-align: center; + margin-right: 20px; + margin-bottom: 20px; + box-shadow: var(--shadow); + background: white; + overflow: hidden; +} + +.current-readings > div > div > div { + padding: 10px 15px; +} + +.current-readings > div > div > div:first-child { + background: var(--light-bg); + border-bottom: 1px solid var(--border-color); + font-weight: bold; +} + +.current-readings > div > div > div:nth-child(2) { + font-weight: bolder; + font-size: 2rem; + border-bottom: 1px solid var(--border-color); + color: var(--primary-color); +} + +.current-readings > div > div > div:last-child { + font-size: 0.9rem; +} diff --git a/templating.py b/templating.py index ce4404f..1e51cd7 100644 --- a/templating.py +++ b/templating.py @@ -51,6 +51,19 @@ class LoginPage(BasePage): ) -> None: super().__init__(jinja_env, userinfo, roominfo, target_path, statictext) +class EmptyRoom(BasePage): + def __init__( + self, + jinja_env, + userinfo, + roominfo, + current_room, + target_path="empty_room.html.jinja", + statictext=None, + ) -> None: + super().__init__(jinja_env, userinfo, roominfo, target_path, statictext) + self.rendervars["current_room"] = current_room + class RoomPage(BasePage): def __init__( self, @@ -66,4 +79,19 @@ class RoomPage(BasePage): super().__init__(jinja_env, userinfo, roominfo, target_path, statictext) self.rendervars["current_room"] = current_room self.rendervars["sensorinfolist"] = sensorinfolist - self.rendervars["fig"] = fig \ No newline at end of file + self.rendervars["fig"] = fig + +class UserManagementPage(BasePage): + def __init__( + self, + jinja_env, + userinfo, + roominfo, + userlist, + admin, + target_path="users.html.jinja", + statictext=None, + ) -> None: + super().__init__(jinja_env, userinfo, roominfo, target_path, statictext) + self.rendervars["userlist"] = userlist + self.rendervars["admin"] = admin \ No newline at end of file diff --git a/user_handling.py b/user_handling.py index 14bfbe1..5fd5c5b 100644 --- a/user_handling.py +++ b/user_handling.py @@ -1,6 +1,6 @@ from os import urandom from typing import Tuple, Optional -from cryptography.fernet import Fernet +from cryptography.fernet import Fernet, InvalidToken from cryptography.hazmat.primitives.kdf.scrypt import Scrypt import base64 @@ -25,17 +25,21 @@ def new_password(password: bytes) -> Tuple[bytes, bytes]: out = f.encrypt(password) return salt, out -def verify_password(stored_salt: bytes, stored_key: bytes, provided_password: bytes) -> bytes: +def verify_password(stored_salt: bytes, stored_key: bytes, provided_password: bytes) -> bool: key = base64.urlsafe_b64encode(__scrypt__(stored_salt).derive(provided_password)) f = Fernet(key) - return f.decrypt(stored_key) + try: + f.decrypt(stored_key) + return True + except InvalidToken: + return False if __name__ == "__main__": - # helper script for inserting new users + # helper script for inserting a new admin import db_connect username = input("Enter new username: ") password = input("Enter new password: ").encode() db = db_connect.DatabaseConnect() salt, key = new_password(password) - db.create_user(username, salt, key) + db.create_user(username, salt, key, True) print(f"Created user {username}") \ No newline at end of file diff --git a/web.py b/web.py index c3a9aa7..e7c8297 100644 --- a/web.py +++ b/web.py @@ -1,7 +1,6 @@ from flask import Flask, abort, request, flash, redirect, get_flashed_messages from flask_login import LoginManager, login_user, logout_user, login_required, current_user -from jinja2 import FileSystemLoader, Template -import plotly.express as px +from jinja2 import FileSystemLoader from plotly.subplots import make_subplots import plotly.graph_objects as go @@ -35,6 +34,18 @@ env = app.create_jinja_environment() DISPLAYEDTYPES = 2 TDLATE = datetime.timedelta(hours=8) +def handle_user_login(username: str, password: str): + user_data = DatabaseConnect().user_by_username(username) + if user_data: + user_id, _, salt, key, _ = user_data + if user_handling.verify_password(salt, key, password.encode()): + user = components.User(user_id, username) + login_user(user) + DatabaseConnect().emit_log(3, f"User {username} logged in successfully.") + return True + DatabaseConnect().emit_log(2, f"Failed login attempt for user {username}.") + return False + @app.route("/login", methods=["GET", "POST"]) def login(): @@ -42,17 +53,9 @@ def login(): username = request.form["username"] password = request.form["password"] - user_data = DatabaseConnect().user_by_username(username) - if user_data: - user_id, _, salt, key = user_data - if user_handling.verify_password(salt, key, password.encode()): - user = components.User(user_id, username) - login_user(user) - DatabaseConnect().emit_log(3, f"User {username} logged in successfully.") - return redirect("/") - DatabaseConnect().emit_log(2, f"Failed login attempt for user {username}.") - flash("Invalid username or password.") - + if handle_user_login(username, password): + return redirect("/") + flash("Invalid username or password.") return templating.LoginPage(env, current_user, roominfo=gather_room_info()).render() @app.route("/logout") @@ -63,15 +66,13 @@ def logout(): @app.route("/") def index(): - rooms_info = gather_room_info() - out_html = templating.LandingPage(env, current_user,rooms_info) return out_html.render() def gather_room_info(): rooms_info = [] - for room in DatabaseConnect().view_valid_rooms(current_user.id): + for room in DatabaseConnect().view_all_rooms() if DatabaseConnect().is_admin(current_user.id) else DatabaseConnect().view_valid_rooms(current_user.id): sensor_info = [] sensors_avail = DatabaseConnect().get_sensors_in_room_id(room[2]) for i in range(DISPLAYEDTYPES): @@ -96,14 +97,21 @@ def gather_room_info(): @app.route('/room/') def room_page(room_name=None): - if not DatabaseConnect().user_has_room_perms(current_user.id,room_name): + if not (DatabaseConnect().user_has_room_perms(current_user.id,room_name) or DatabaseConnect().is_admin(current_user.id)): abort(403) roominfos = gather_room_info() current_room = next((x for x in roominfos if x.shortcode == room_name), None) - #TODO: if statement to failsafe room with no sensors sensor_list = DatabaseConnect().get_sensors_in_room_shortname(room_name) + if not sensor_list: + return templating.EmptyRoom( + env, + current_user, + roominfos, + current_room + ).render() + fig = make_subplots(rows=1, cols=len(sensor_list)) lastest_readings = [] for idx, (sensorID, sensor_type) in enumerate(sensor_list): @@ -115,10 +123,22 @@ def room_page(room_name=None): lst[0][0], f"{lst[0][1]:.2f}{DatabaseConnect().get_unit_for_type(sensor_type) or ''}" )) + x = [] + y = [] + last = None + for i in lst: + if last is not None and (last - i[0]) > TDLATE: + x.append(None) + y.append(last + datetime.timedelta(seconds=1)) + x.append(i[0]) + y.append(i[1]) + last = i[0] + fig.add_trace( go.Scatter( - x = [x[0] for x in lst], - y = [x[1] for x in lst], + connectgaps=False, + x = x, + y = y, name=DatabaseConnect().get_sensor_type(sensorID)), row = 1, col = idx + 1 @@ -135,6 +155,51 @@ def room_page(room_name=None): fig.to_html(full_html=False, include_plotlyjs='cdn') ).render() +@app.route("/users", methods=["GET","POST"]) +def user_management(): + if request.method == "POST": + # no options allowed for logged out users + if current_user.id == 1: + abort(403) + + if "old_password" in request.form: + # password change request + old_password = request.form["old_password"] + new_password = request.form["new_password"] + if handle_user_login(current_user.username, old_password): + salt, key = user_handling.new_password(new_password.encode()) + DatabaseConnect().change_user_password(current_user.id, salt, key) + flash("Password changed successfully.") + return redirect("/users") + else: + flash("Old password incorrect.") + return redirect("/users") + else: + if not DatabaseConnect().is_admin(current_user.id): + abort(403) + elif "new_username" in request.form: + new_username = request.form["new_username"] + new_password = request.form["new_password"] + is_admin = request.form.get("is_admin") == "on" + salt, key = user_handling.new_password(new_password.encode()) + DatabaseConnect().create_user(new_username, salt, key, is_admin) + flash(f"Created user {new_username}.") + return redirect("/users") + elif "delete" in request.form: + value = int(request.form["delete"]) + if value == current_user.id: + flash("Cannot delete currently logged in user.") + return redirect("/users") + DatabaseConnect().delete_user(value) + flash(f"Deleted user ID {value}.") + return redirect("/users") + else: + abort(400) + else: + users = DatabaseConnect().display_users() + admin = DatabaseConnect().is_admin(current_user.id) + return templating.UserManagementPage(env, current_user, gather_room_info(), users, admin).render() + if __name__ == "__main__": app.run(debug=True) \ No newline at end of file diff --git a/web/base.html.jinja b/web/base.html.jinja index e99efd5..1384667 100644 --- a/web/base.html.jinja +++ b/web/base.html.jinja @@ -13,12 +13,12 @@
diff --git a/web/empty_room.html.jinja b/web/empty_room.html.jinja new file mode 100644 index 0000000..a9618dd --- /dev/null +++ b/web/empty_room.html.jinja @@ -0,0 +1,8 @@ +{% extends "base.html.jinja" %} +{% block content %} + +
+

Room: {{ current_room.name }}

+

This room has no sensors assigned to it.

+
+{% endblock content %} \ No newline at end of file diff --git a/web/index.html.jinja b/web/index.html.jinja index b3a958e..65e7883 100644 --- a/web/index.html.jinja +++ b/web/index.html.jinja @@ -33,15 +33,18 @@
diff --git a/web/landing.html.jinja b/web/landing.html.jinja index d366290..eb9da86 100644 --- a/web/landing.html.jinja +++ b/web/landing.html.jinja @@ -2,8 +2,10 @@ {% block content %}
-

{{ user_greeting_before }}{{userinfo.username}}{{user_greeting_after}}

-

{{ landing_information }}

+
+

{{ user_greeting_before }}{{userinfo.username}}{{user_greeting_after}}

+

{{ landing_information }}

+
@@ -14,8 +16,7 @@ diff --git a/web/room.html.jinja b/web/room.html.jinja index f5639b9..c01d97e 100644 --- a/web/room.html.jinja +++ b/web/room.html.jinja @@ -26,8 +26,8 @@ {% endfor %} - {{ fig }} + {{ fig }} {% endblock content %} \ No newline at end of file diff --git a/web/users.html.jinja b/web/users.html.jinja new file mode 100644 index 0000000..008e4c3 --- /dev/null +++ b/web/users.html.jinja @@ -0,0 +1,81 @@ +{% extends "base.html.jinja" %} + +{% block content %} + +
+

User Management

+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +
+ + +
+ + +
+ + + {% if admin %} +
+

Add New User:

+
+ + +
+ + +
+ + +
+ + +
+
+

Existing Users:

+
+ + + + + + + + + + + + + {% for tableuser in userlist %} + + + + + {% if (tableuser[0] == 1) or (tableuser[0] == userinfo.id) %} + + + {% else %} + + {% endif %} + + {% endfor %} + +
User IDUsernameAdmin status
{{ tableuser[0] }} {{ tableuser[1] }} {{ tableuser[2] }}
Cannot Delete
+
+ +
+
+ {% else %} + {% endif %} + + +{% endblock content %} \ No newline at end of file