user handling

This commit is contained in:
2025-12-15 23:07:19 +01:00
parent f6f2d20eb8
commit b0f19eb72e
15 changed files with 573 additions and 127 deletions

View File

@@ -12,7 +12,6 @@ class AnonymousUser(AnonymousUserMixin):
self.id = 1 self.id = 1
self.username = "nouser" self.username = "nouser"
UserInfo = namedtuple('UserInfo',['id', 'name'])
RoomInfo = namedtuple('RoomInfo',['name', 'shortcode', 'roomdata']) RoomInfo = namedtuple('RoomInfo',['name', 'shortcode', 'roomdata'])
# state is an int not in db # state is an int not in db
# 0 - value valid # 0 - value valid

View File

@@ -38,33 +38,52 @@ class DatabaseConnect:
case 3: case 3:
logging.info(f"Log emitted: {message}") logging.info(f"Log emitted: {message}")
def create_user(self, username: str, salt: bytes, key: bytes) -> None: def create_user(self, username: str, salt: bytes, key: bytes, administer: bool = False) -> None:
self.cursor.execute("INSERT INTO `users` (`username`, `salt`, `key`) VALUES (?,?,?)",(username, salt, key)) self.cursor.execute("INSERT INTO `users` (`username`, `salt`, `key`, `administer`) VALUES (?,?,?,?)",(username, salt, key, administer))
self.conn.commit() self.conn.commit()
self.emit_log(2, f"Created user {username}") 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: 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}") self.emit_log(2, f"Deleted user ID {id}")
def display_users(self) -> tuple[tuple[int, str, int]]: def display_users(self) -> tuple[tuple[int, str, bool]]:
self.cursor.execute("SELECT * FROM Users") self.cursor.execute("SELECT users.`ID`, users.`username`, users.`administer` FROM Users")
return self.cursor.fetchall() return self.cursor.fetchall()
def username_from_id(self, user_id): def username_from_id(self, user_id):
self.cursor.execute("SELECT users.`username` FROM users WHERE `ID` = ?;", (user_id,)) self.cursor.execute("SELECT users.`username` FROM users WHERE `ID` = ?;", (user_id,))
return self.cursor.fetchone() 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,)) self.cursor.execute("SELECT * FROM users WHERE `username` = ?;", (username,))
return self.cursor.fetchone() 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]]: def view_valid_rooms(self, user_id) -> list[tuple[str, str, int]]:
self.cursor.execute( self.cursor.execute(
"SELECT rooms.name, rooms.shortname, rooms.ID from permissions INNER JOIN rooms ON permissions.`roomID` = rooms.`ID` WHERE permissions.`userID` = ? AND permissions.`view` = 1;", "SELECT rooms.name, rooms.shortname, rooms.ID from permissions INNER JOIN rooms ON permissions.`roomID` = rooms.`ID` WHERE permissions.`userID` = ? AND permissions.`view` = 1;",
(user_id,)) (user_id,))
return self.cursor.fetchall() 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: 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)) 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() res = self.cursor.fetchone()
@@ -78,11 +97,11 @@ class DatabaseConnect:
# return self.cursor.fetchone() # return self.cursor.fetchone()
def get_sensors_in_room_shortname(self, shortname) -> list[tuple[int, int]]: 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()] return [x for x in self.cursor.fetchall()]
def get_sensors_in_room_id(self, roomid) -> list[tuple[int, int]]: 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()] return [(x[0], x[1]) for x in self.cursor.fetchall()]
def get_sensorsinfo_by_sensorid(self, sensorid) -> SensorInfo | None: def get_sensorsinfo_by_sensorid(self, sensorid) -> SensorInfo | None:

View File

@@ -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])) db_connect.DatabaseConnect().create_sensor_reading(id, datetime.datetime.fromisoformat(data[0]), float(data[1]))
else: else:
db_connect.DatabaseConnect().emit_log(1, f"Received MQTT message for unknown topic {topic}") 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() @fast_mqtt.on_disconnect()
def on_disconnect(client: MQTTClient, packet, exc=None): def on_disconnect(client: MQTTClient, packet, exc=None):

View File

@@ -4,23 +4,32 @@ GRANT ALL PRIVILEGES ON sensors.* TO SensorsAdmin;
USE sensors; USE sensors;
GO GO
CREATE TABLE Users (`ID` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `username` TEXT, `pwd` INT); 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`, `pwd`) VALUES (1,'nouser',NULL), (NULL,'Admin',1); 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 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 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 TABLE Readings(`sensorID` INT, `Timestamp` DATETIME, `reading` DOUBLE);
CREATE INDEX `sensor_index` ON Readings (`sensorID`); CREATE INDEX `sensor_index` ON Readings (`sensorID`);
CREATE TABLE Log( `timestamp` TIMESTAMP, `type` INT, `message` TEXT); 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 Rooms (`name`,`shortname`) VALUES ('101','101'),('102','102'),('210','210'),('211','211'),('215 - Studovna','215')('000','Venku');
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 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 Types (type_desc) VALUES ("Temperature"), ('Humidity');
INSERT INTO `readings`(`Timestamp`,`reading`,`sensorID`) VALUES 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), ('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),

View File

@@ -1,11 +1,11 @@
function tableSearch(){ function tableSearch(own_elem, tableID, rowsearch=0) {
var table, input, tr, td, row_content; var table, input, tr, td, row_content;
table = document.getElementById("tableRooms"); table = document.getElementById(tableID);
input = document.getElementById("tableRoomSearch").value.toUpperCase(); input = document.getElementById(own_elem).value.toUpperCase();
tr = table.getElementsByTagName("tr"); tr = table.getElementsByTagName("tr");
for (let i = 0; i < tr.length; i++) { for (let i = 0; i < tr.length; i++) {
td = tr[i].getElementsByTagName("td")[0]; td = tr[i].getElementsByTagName("td")[rowsearch];
if(td){ if(td){
row_content = td.textContent || td.innerText; row_content = td.textContent || td.innerText;
if (row_content.toUpperCase().indexOf(input) > -1) { if (row_content.toUpperCase().indexOf(input) > -1) {

View File

@@ -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; text-decoration: none;
transition: var(--transition);
font-weight: bold;
} }
.main-flex{ a:hover {
color: var(--accent-color);
text-decoration: underline;
}
.main-flex {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
background: white;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
min-height: calc(100vh - 40px);
} }
.navbar{ .navbar {
flex: 1 10vw; flex: 0 0 250px;
white-space: nowrap; background: var(--dark-bg);
} color: white;
padding: 20px;
.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{
display: flex; 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; 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{ .navbar-inner a:hover {
padding-left: 1em; background: var(--primary-color);
} }
.rooms-table{ .navbar-inner ul {
background-color: lightgray; list-style: none;
padding: 0.5em 1.5em; padding-left: 20px;
width: min-content;
} }
.rooms-table table{ .navbar-inner li {
width: 50vw; margin-bottom: 5px;
background-color: inherit;
text-align: center;
border-color: darkgray;
border-collapse: collapse;
border-left: 10px;
} }
.rooms-table tr{
padding-left: 1em;
}
.rooms-table td{
padding-bottom: 0.2em;
padding-top: 0.2em;
}
thead{
border-bottom: 0.25em darkgray solid;
.navbar-inner li a {
padding: 5px 0;
} }
tbody tr{
border-bottom: 0.1em darkgray solid; .right-flex {
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
flex-grow: 9;
} }
.data-missing{
background-color: gray; .topline {
color: transparent display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
} }
.data-late{
background-color: orange; .intro-text{
margin-bottom: 20px;
} }
.room-table-elem, .searchbar{
.user {
font-weight: bold; font-weight: bold;
text-align: left;
} }
.rooms-table input{ .logout a {
width: 90%; 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-valid,
.current-readings .data-late, .current-readings .data-late,
.current-readings .data-missing{ .current-readings .data-missing {
padding: 0.2em 1em; 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;
} }

View File

@@ -51,6 +51,19 @@ class LoginPage(BasePage):
) -> None: ) -> None:
super().__init__(jinja_env, userinfo, roominfo, target_path, statictext) 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): class RoomPage(BasePage):
def __init__( def __init__(
self, self,
@@ -67,3 +80,18 @@ class RoomPage(BasePage):
self.rendervars["current_room"] = current_room self.rendervars["current_room"] = current_room
self.rendervars["sensorinfolist"] = sensorinfolist self.rendervars["sensorinfolist"] = sensorinfolist
self.rendervars["fig"] = fig 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

View File

@@ -1,6 +1,6 @@
from os import urandom from os import urandom
from typing import Tuple, Optional from typing import Tuple, Optional
from cryptography.fernet import Fernet from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
import base64 import base64
@@ -25,17 +25,21 @@ def new_password(password: bytes) -> Tuple[bytes, bytes]:
out = f.encrypt(password) out = f.encrypt(password)
return salt, out 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)) key = base64.urlsafe_b64encode(__scrypt__(stored_salt).derive(provided_password))
f = Fernet(key) f = Fernet(key)
return f.decrypt(stored_key) try:
f.decrypt(stored_key)
return True
except InvalidToken:
return False
if __name__ == "__main__": if __name__ == "__main__":
# helper script for inserting new users # helper script for inserting a new admin
import db_connect import db_connect
username = input("Enter new username: ") username = input("Enter new username: ")
password = input("Enter new password: ").encode() password = input("Enter new password: ").encode()
db = db_connect.DatabaseConnect() db = db_connect.DatabaseConnect()
salt, key = new_password(password) salt, key = new_password(password)
db.create_user(username, salt, key) db.create_user(username, salt, key, True)
print(f"Created user {username}") print(f"Created user {username}")

101
web.py
View File

@@ -1,7 +1,6 @@
from flask import Flask, abort, request, flash, redirect, get_flashed_messages 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 flask_login import LoginManager, login_user, logout_user, login_required, current_user
from jinja2 import FileSystemLoader, Template from jinja2 import FileSystemLoader
import plotly.express as px
from plotly.subplots import make_subplots from plotly.subplots import make_subplots
import plotly.graph_objects as go import plotly.graph_objects as go
@@ -35,6 +34,18 @@ env = app.create_jinja_environment()
DISPLAYEDTYPES = 2 DISPLAYEDTYPES = 2
TDLATE = datetime.timedelta(hours=8) 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"]) @app.route("/login", methods=["GET", "POST"])
def login(): def login():
@@ -42,17 +53,9 @@ def login():
username = request.form["username"] username = request.form["username"]
password = request.form["password"] password = request.form["password"]
user_data = DatabaseConnect().user_by_username(username) if handle_user_login(username, password):
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("/") return redirect("/")
DatabaseConnect().emit_log(2, f"Failed login attempt for user {username}.")
flash("Invalid username or password.") flash("Invalid username or password.")
return templating.LoginPage(env, current_user, roominfo=gather_room_info()).render() return templating.LoginPage(env, current_user, roominfo=gather_room_info()).render()
@app.route("/logout") @app.route("/logout")
@@ -63,15 +66,13 @@ def logout():
@app.route("/") @app.route("/")
def index(): def index():
rooms_info = gather_room_info() rooms_info = gather_room_info()
out_html = templating.LandingPage(env, current_user,rooms_info) out_html = templating.LandingPage(env, current_user,rooms_info)
return out_html.render() return out_html.render()
def gather_room_info(): def gather_room_info():
rooms_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 = [] sensor_info = []
sensors_avail = DatabaseConnect().get_sensors_in_room_id(room[2]) sensors_avail = DatabaseConnect().get_sensors_in_room_id(room[2])
for i in range(DISPLAYEDTYPES): for i in range(DISPLAYEDTYPES):
@@ -96,14 +97,21 @@ def gather_room_info():
@app.route('/room/<room_name>') @app.route('/room/<room_name>')
def room_page(room_name=None): 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) abort(403)
roominfos = gather_room_info() roominfos = gather_room_info()
current_room = next((x for x in roominfos if x.shortcode == room_name), None) 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) 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)) fig = make_subplots(rows=1, cols=len(sensor_list))
lastest_readings = [] lastest_readings = []
for idx, (sensorID, sensor_type) in enumerate(sensor_list): for idx, (sensorID, sensor_type) in enumerate(sensor_list):
@@ -115,10 +123,22 @@ def room_page(room_name=None):
lst[0][0], lst[0][0],
f"{lst[0][1]:.2f}{DatabaseConnect().get_unit_for_type(sensor_type) or ''}" 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( fig.add_trace(
go.Scatter( go.Scatter(
x = [x[0] for x in lst], connectgaps=False,
y = [x[1] for x in lst], x = x,
y = y,
name=DatabaseConnect().get_sensor_type(sensorID)), name=DatabaseConnect().get_sensor_type(sensorID)),
row = 1, row = 1,
col = idx + 1 col = idx + 1
@@ -135,6 +155,51 @@ def room_page(room_name=None):
fig.to_html(full_html=False, include_plotlyjs='cdn') fig.to_html(full_html=False, include_plotlyjs='cdn')
).render() ).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__": if __name__ == "__main__":
app.run(debug=True) app.run(debug=True)

View File

@@ -13,12 +13,12 @@
<div class="navbar"> <div class="navbar">
<div class="topbranding">{{ top_branding }}</div> <div class="topbranding">{{ top_branding }}</div>
<div class="navbar-inner"> <div class="navbar-inner">
<b><a href="/">Home</a></b><br> <div><b><a href="/">Home</a></b></div>
<div id="rooms-rolled" onclick="toggleRoomDropdown()"> <div id="rooms-rolled" onclick="toggleRoomDropdown()">
<i class="bi bi-arrow-right-square-fill">&nbsp;</i>Rooms<br> <i class="bi bi-arrow-right-square-fill">&nbsp;</i>Rooms
</div> </div>
<div id="rooms-unrolled" style="display: none;" onclick="untoggleRoomDropdown()"> <div id="rooms-unrolled" style="display: none;" onclick="untoggleRoomDropdown()">
<i class="bi bi-arrow-down-square-fill">&nbsp;</i>Rooms<br> <i class="bi bi-arrow-down-square-fill">&nbsp;</i>Rooms
<ul> <ul>
{% for room in roominfo %} {% for room in roominfo %}
<li><a href="/room/{{ room.shortcode }}">{{ room.name }}</a></li> <li><a href="/room/{{ room.shortcode }}">{{ room.name }}</a></li>
@@ -26,8 +26,8 @@
</ul> </ul>
</div> </div>
<hr> <hr>
<a href="device.html">Device Management</a><br> <div><a href="/devices">Device Management</a></div>
<a href="users.html">User Management</a> <div><a href="/users">User Management</a></div>
</div> </div>
</div> </div>
<div class="right-flex"> <div class="right-flex">

View File

@@ -0,0 +1,8 @@
{% extends "base.html.jinja" %}
{% block content %}
<div class="content">
<h2>Room: {{ current_room.name }}</h2>
<p>This room has no sensors assigned to it.</p>
</div>
{% endblock content %}

View File

@@ -33,15 +33,18 @@
<div class="navbar"> <div class="navbar">
<div class="topbranding">{{ top_branding }}</div> <div class="topbranding">{{ top_branding }}</div>
<div class="navbar-inner"> <div class="navbar-inner">
<b><a href="index.html">Home</a></b><br> <div><b><a href="index.html">Home</a></b></div>
<i class="bi bi-arrow-down-square-fill">&nbsp;</i>Rooms<br> <div><i class="bi bi-arrow-down-square-fill">&nbsp;</i>Rooms</div>
<!--{%- for item in rooms %} <!--{%- for item in rooms %}
&emsp;<a href="{{ item[0] }}">{{ item[1] }}</a>{% if not loop.last %},{% endif %} &emsp;<a href="{{ item[0] }}">{{ item[1] }}</a>{% if not loop.last %},{% endif %}
{%- endfor %}--> {%- endfor %}-->
<hr> <hr>
<!--{%- if user.device_privileges % and so on}--> <!--{%- if user.device_privileges % and so on}-->
<a href="device.html">Device Management</a><br> <div><a href="/devices.html">Device Management</a></div>
<a href="users.html">User Management</a> {% if user.id != 1%}
<div><a href="/users.html">User Management</a></div>
{% else %}
{% endif %}
</div> </div>
</div> </div>
<div class="right-flex"> <div class="right-flex">

View File

@@ -2,8 +2,10 @@
{% block content %} {% block content %}
<div class="content"> <div class="content">
<div class="intro-text">
<p> {{ user_greeting_before }}{{userinfo.username}}{{user_greeting_after}}</p> <p> {{ user_greeting_before }}{{userinfo.username}}{{user_greeting_after}}</p>
<p> {{ landing_information }} </p> <p> {{ landing_information }} </p>
</div>
<div class="rooms-table"> <div class="rooms-table">
<table> <table>
<thead> <thead>
@@ -14,8 +16,7 @@
</tr> </tr>
<tr class="searchbar"> <tr class="searchbar">
<td colspan="255"> <td colspan="255">
<i class="bi bi-search"></i>&nbsp; <input type="text" id="tableRoomSearch" placeholder="Search by room name..." onkeyup="tableSearch('tableRooms', 'tableRoomSearch')">
<input type="text" id="tableRoomSearch" placeholder="Search by room name..." onkeyup="tableSearch()">
</td> </td>
</tr> </tr>
</thead> </thead>

View File

@@ -26,8 +26,8 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{{ fig }}
</div> </div>
{{ fig }}
</div> </div>
{% endblock content %} {% endblock content %}

81
web/users.html.jinja Normal file
View File

@@ -0,0 +1,81 @@
{% extends "base.html.jinja" %}
{% block content %}
<div class="content">
<h2>User Management</h2>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class="flashes">
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<form method="POST" action="/users">
<label for="username">Old password:</label>
<input type="text" id="old_password" name="old_password" required>
<br>
<label for="password">New Password:</label>
<input type="password" id="new_password" name="new_password" required>
<br>
<button type="submit">Change Password</button>
</form>
{% if admin %}
<div class="add-user">
<h3>Add New User:</h3>
<form method="POST" action="/users">
<label for="new_username">Username:</label>
<input type="text" id="new_username" name="new_username" required>
<br>
<label for="new_password">Password:</label>
<input type="password" id="new_password" name="new_password" required>
<br>
<label for="is_admin">Admin Privileges:</label>
<input type="checkbox" id="is_admin" name="is_admin">
<br>
<button type="submit">Add User</button>
</form>
</div>
<div class="users-list">
<h3>Existing Users:</h3>
<table>
<thead>
<tr class="table-header">
<th>User ID</th>
<th>Username</th>
<th>Admin status</th>
<th></th>
</tr>
<tr class="searchbar">
<td colspan="255">
<input type="text" id="userSearch" placeholder="Search by user name..." onkeyup="tableSearch('userSearch', 'userlist',1)">
</td>
</tr>
</thead>
<tbody id="userlist">
{% for tableuser in userlist %}
<tr>
<td> {{ tableuser[0] }} </td>
<td> {{ tableuser[1] }} </td>
<td> {{ tableuser[2] }} </td>
{% if (tableuser[0] == 1) or (tableuser[0] == userinfo.id) %}
<!-- TODO: delete self prevention still does not work -->
<td><div class="fake-button">Cannot Delete</div></td>
{% else %}
<td>
<form method="POST" action="/users">
<button name="delete" value="{{ tableuser[0] }}", type="submit">Delete User</button>
</form>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
{% endif %}
{% endblock content %}