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.username = "nouser"
UserInfo = namedtuple('UserInfo',['id', 'name'])
RoomInfo = namedtuple('RoomInfo',['name', 'shortcode', 'roomdata'])
# state is an int not in db
# 0 - value valid

View File

@@ -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:

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]))
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):

View File

@@ -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),

View File

@@ -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) {

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;
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;
}
.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;
}

View File

@@ -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
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 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}")

105
web.py
View File

@@ -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/<room_name>')
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)

View File

@@ -13,12 +13,12 @@
<div class="navbar">
<div class="topbranding">{{ top_branding }}</div>
<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()">
<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 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>
{% for room in roominfo %}
<li><a href="/room/{{ room.shortcode }}">{{ room.name }}</a></li>
@@ -26,8 +26,8 @@
</ul>
</div>
<hr>
<a href="device.html">Device Management</a><br>
<a href="users.html">User Management</a>
<div><a href="/devices">Device Management</a></div>
<div><a href="/users">User Management</a></div>
</div>
</div>
<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="topbranding">{{ top_branding }}</div>
<div class="navbar-inner">
<b><a href="index.html">Home</a></b><br>
<i class="bi bi-arrow-down-square-fill">&nbsp;</i>Rooms<br>
<div><b><a href="index.html">Home</a></b></div>
<div><i class="bi bi-arrow-down-square-fill">&nbsp;</i>Rooms</div>
<!--{%- for item in rooms %}
&emsp;<a href="{{ item[0] }}">{{ item[1] }}</a>{% if not loop.last %},{% endif %}
{%- endfor %}-->
<hr>
<!--{%- if user.device_privileges % and so on}-->
<a href="device.html">Device Management</a><br>
<a href="users.html">User Management</a>
<div><a href="/devices.html">Device Management</a></div>
{% if user.id != 1%}
<div><a href="/users.html">User Management</a></div>
{% else %}
{% endif %}
</div>
</div>
<div class="right-flex">

View File

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

View File

@@ -26,8 +26,8 @@
</div>
{% endfor %}
</div>
{{ fig }}
</div>
{{ fig }}
</div>
{% 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 %}