user handling
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -38,33 +38,52 @@ 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(
|
||||
"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,))
|
||||
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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,98 +1,326 @@
|
||||
*{
|
||||
font-family: sans-serif;
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
flex: 0 0 250px;
|
||||
background: var(--dark-bg);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.topbranding, .navbar-inner, .topline{
|
||||
.topbranding {
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
border: solid 0.2em;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
padding-bottom: 0.25em;
|
||||
padding-top: 0.25em;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
background: var(--primary-color);
|
||||
border-radius: var(--border-radius);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.navbar-inner {
|
||||
font-weight: normal;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.navbar-inner a {
|
||||
color: white;
|
||||
display: block;
|
||||
padding: 10px 0;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.navbar-inner a:hover {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.navbar-inner ul {
|
||||
list-style: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.navbar-inner li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.navbar-inner li a {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.right-flex {
|
||||
flex: 1 60vw;
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 9;
|
||||
}
|
||||
|
||||
.topline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.section{
|
||||
flex: 1;
|
||||
|
||||
.intro-text{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.logout{
|
||||
text-align: right;
|
||||
|
||||
.user {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logout a {
|
||||
color: var(--primary-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-left: 1em;
|
||||
flex: 1;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.rooms-table {
|
||||
background-color: lightgray;
|
||||
padding: 0.5em 1.5em;
|
||||
width: min-content;
|
||||
background: var(--light-bg);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.rooms-table table{
|
||||
width: 50vw;
|
||||
background-color: inherit;
|
||||
text-align: center;
|
||||
border-color: darkgray;
|
||||
table {
|
||||
width: 100%;
|
||||
background: white;
|
||||
border-collapse: collapse;
|
||||
border-left: 10px;
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.rooms-table tr{
|
||||
padding-left: 1em;
|
||||
}
|
||||
.rooms-table td{
|
||||
padding-bottom: 0.2em;
|
||||
padding-top: 0.2em;
|
||||
}
|
||||
thead{
|
||||
border-bottom: 0.25em darkgray solid;
|
||||
|
||||
}
|
||||
tbody tr{
|
||||
border-bottom: 0.1em darkgray solid;
|
||||
}
|
||||
.data-missing{
|
||||
background-color: gray;
|
||||
color: transparent
|
||||
}
|
||||
.data-late{
|
||||
background-color: orange;
|
||||
}
|
||||
.room-table-elem, .searchbar{
|
||||
font-weight: bold;
|
||||
th, td {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.rooms-table input{
|
||||
width: 90%;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
@@ -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,
|
||||
@@ -67,3 +80,18 @@ class RoomPage(BasePage):
|
||||
self.rendervars["current_room"] = current_room
|
||||
self.rendervars["sensorinfolist"] = sensorinfolist
|
||||
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
|
||||
@@ -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}")
|
||||
101
web.py
101
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.")
|
||||
if handle_user_login(username, password):
|
||||
return redirect("/")
|
||||
DatabaseConnect().emit_log(2, f"Failed login attempt for user {username}.")
|
||||
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)
|
||||
@@ -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"> </i>Rooms<br>
|
||||
<i class="bi bi-arrow-right-square-fill"> </i>Rooms
|
||||
</div>
|
||||
<div id="rooms-unrolled" style="display: none;" onclick="untoggleRoomDropdown()">
|
||||
<i class="bi bi-arrow-down-square-fill"> </i>Rooms<br>
|
||||
<i class="bi bi-arrow-down-square-fill"> </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">
|
||||
|
||||
8
web/empty_room.html.jinja
Normal file
8
web/empty_room.html.jinja
Normal 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 %}
|
||||
@@ -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"> </i>Rooms<br>
|
||||
<div><b><a href="index.html">Home</a></b></div>
|
||||
<div><i class="bi bi-arrow-down-square-fill"> </i>Rooms</div>
|
||||
<!--{%- for item in rooms %}
|
||||
 <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">
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="content">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ fig }}
|
||||
</div>
|
||||
{{ fig }}
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
81
web/users.html.jinja
Normal file
81
web/users.html.jinja
Normal 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 %}
|
||||
Reference in New Issue
Block a user