Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# JSON files
devices.json
selected_user.json

# Venv files
venv/

Python cache files
# Python cache files
__pycache__/

# Secrets
Expand Down
30 changes: 26 additions & 4 deletions backend/fastAPI.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import devices_json as dj
import users

import asyncio
import os
import json
Expand All @@ -16,7 +18,7 @@
# Add CORS Middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
Expand Down Expand Up @@ -51,8 +53,13 @@ def change_device_name(id: int, new_name: str):

@app.get("/updates")
def get_updates():
"""Returns the updates list."""
return {"updates": dj.getUpdates()}
"""Returns combined updates from devices and users."""
device_updates = dj.getUpdates()
user_updates = users.getUpdates()

all_updates = device_updates + user_updates

return {"updates": all_updates}

@app.post("/device/{id}/connect")
async def change_connection_status(id: int):
Expand All @@ -75,4 +82,19 @@ def get_user_data():
@app.get("/device_functions")
def get_device_functions():
"""Returns the list of device functions."""
return {"functions": dj.deviceFunctions()}
return {"functions": dj.deviceFunctions()}

@app.post("/select_user/{user}")
def set_selected_user(user: str):
"""Sets the selected user"""
return users.select_user(user)

@app.get("/selected_user")
def get_selected_user():
"""Returns the selected user"""
return users.get_selected_user()

@app.get("/updates/users")
def get_updates():
"""Returns the updates list."""
return {"updates": users.getUpdates()}
31 changes: 30 additions & 1 deletion backend/users.py
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
selected_user = ""
import json
import os

SELECTED_USER_FILE = "selected_user.json"
updates = []

def select_user(user: str):
"""Set the selected user and persist it."""
global selected_user
selected_user = user
with open(SELECTED_USER_FILE, "w") as f:
json.dump({"selected_user": selected_user}, f)
message = f"Logged in as {selected_user}"
updates.append(message)
return {"success": message}

def get_selected_user():
"""Retrieve the currently selected user."""
global selected_user
if os.path.exists(SELECTED_USER_FILE):
with open(SELECTED_USER_FILE, "r") as f:
data = json.load(f)
selected_user = data.get("selected_user", "")
return {"selected_user": selected_user}

def getUpdates():
global updates
messages = updates[:]
updates.clear()
return messages
7 changes: 7 additions & 0 deletions database/dailyenergy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"daily energy": [
{
"device_id": 1,
"date":"24/02/2025",
"energy":
},
1 change: 0 additions & 1 deletion database/devices_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,3 @@ def start_file_watcher():
if __name__ == "__main__":
print("Starting file watcher for devices.json...")
start_file_watcher()

142 changes: 118 additions & 24 deletions database/users_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,127 @@
from firebase_admin import credentials, firestore
import json
import os
#rom dotenv import load_dotenv
import bcrypt
import time
from dotenv import load_dotenv
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

# Load environment variables
load_dotenv()

# Read Firebase credentials from .env
firebase_config = {
"type": "service_account",
"project_id": os.getenv("FIREBASE_PROJECT_ID"),
"private_key": (os.getenv("FIREBASE_PRIVATE_KEY") or "").replace("\\n", "\n"),
"client_email": os.getenv("FIREBASE_CLIENT_EMAIL"),
"token_uri": "https://oauth2.googleapis.com/token"
}

# Initialize Firebase Admin SDK if not already initialized
if not firebase_admin._apps:
cred = credentials.Certificate(firebase_config)
firebase_admin.initialize_app(cred)

cred = credentials.Certificate("/Users/annerinjeri/Downloads/powerhouse-62f4d-firebase-adminsdk-fbsvc-9a2e946c98.json")
firebase_admin.initialize_app(cred)
db = firestore.client()

#load_dotenv()
def_set_doc_data(collref,)
# Load JSON file
with open("users.json", "r") as file:
data = json.load(file)

# Upload users to Firestore with device references
for user in data["users"]:
user_id = user["user_name"].replace(" ", "_").lower() # Unique Firestore ID
user_ref = db.collection("Users").document(user_id)

# Convert device IDs into Firestore document references
device_refs = [db.collection("Devices").document(device_id) for device_id in user["allocated_devices"]]

user_data = {
"user_name": user["user_name"],
"user_password": user["user_password"], # Password stored as plain text (⚠ Not recommended for production)
"allocated_devices": device_refs, # Store Firestore document references
"user_role": user["user_role"]
# Hash password function
def hash_password(password):
"""Hash password using bcrypt."""
salt = bcrypt.gensalt()
return bcrypt.hashpw(password.encode(), salt).decode()

# Get existing users from Firestore
def get_existing_users(coll_ref):
"""Fetch existing users from Firestore."""
return {doc.id: doc.to_dict() for doc in coll_ref.stream()}

# Function to update Firestore
def set_users_data(coll_ref, file):
"""Compare JSON data with Firestore and update only necessary records."""
rel_path = os.path.join(os.path.dirname(__file__), f"../database/{file}")

if not os.path.exists(rel_path):
print(f"Error: {file} not found at {rel_path}")
return

with open(rel_path, "r") as JSONfile:
data = json.load(JSONfile)

# Convert allocated device IDs to Firestore document references
def get_device_refs(device_ids):
return [db.collection("Devices").document(str(device_id)) for device_id in device_ids]

filtered_users = {
str(user["user_id"]): {
"user_id": user["user_id"],
"user_name": user["user_name"],
"user_password": hash_password(user["user_password"]), # Hash password
"allocated_devices": get_device_refs(user.get("allocated_devices", [])),
"user_role": user["user_role"]
}
for user in data.get("users", [])
}

user_ref.set(user_data)
print(f"User {user['user_name']} added with device references!")
existing_users = get_existing_users(coll_ref)

added_users, modified_users, deleted_users = [], [], []

# Add or update users
for user_id, user_data in filtered_users.items():
if user_id not in existing_users:
coll_ref.document(user_id).set(user_data)
added_users.append(user_data)
elif existing_users[user_id] != user_data:
coll_ref.document(user_id).set(user_data)
modified_users.append(user_data)

# Delete outdated users no longer in the JSON
for user_id in existing_users.keys():
if user_id not in filtered_users:
coll_ref.document(user_id).delete()
deleted_users.append(user_id)

# Logging changes
for user in added_users:
print(f"Added user: {user}")
for user in modified_users:
print(f"Updated user: {user}")
for user_id in deleted_users:
print(f"Deleted outdated user: {user_id}")

# Watchdog Event Handler
class UsersFileHandler(FileSystemEventHandler):
def on_modified(self, event):
"""Triggered when users_db.json is modified"""
if event.src_path.endswith("users_db.json"):
print("Detected changes in users_db.json, updating Firestore...")
set_users_data(db.collection("Users"), "users_db.json")

# Watchdog Observer
def watch_users_file():
"""Monitor users_db.json for changes and update Firestore in real-time."""
users_file_path = os.path.join(os.path.dirname(__file__), "../database")

event_handler = UsersFileHandler()
observer = Observer()
observer.schedule(event_handler, path=users_file_path, recursive=False)

observer.start()
print("Watching for changes in users_db.json...")

try:
while True:
time.sleep(1) # Keep script running
except KeyboardInterrupt:
observer.stop()

observer.join()

# Run initial Firestore sync
print("Syncing Firestore with users_db.json on startup...")
set_users_data(db.collection("Users"), "users_db.json")

# Run the watchdog file watcher
watch_users_file()
29 changes: 26 additions & 3 deletions frontend/src/app/ui/dashboard/accountMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ import { useRouter } from "next/navigation";
export default function AccountMenu() {
const router = useRouter();
const [anchorEl, setAnchorEl] = React.useState(null);
const [selectedUser, setSelectedUser] = React.useState("Loading...");

const open = Boolean(anchorEl);

const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};

const handleClose = () => {
setAnchorEl(null);
};
Expand All @@ -37,7 +41,26 @@ export default function AccountMenu() {
console.error("Sign out error:", error);
}
};


// Fetch selected user from the backend
React.useEffect(() => {
const fetchSelectedUser = async () => {
try {
const response = await fetch("http://localhost:8000/selected_user");
if (!response.ok) {
throw new Error("Failed to fetch user");
}
const data = await response.json();
setSelectedUser(data.selected_user || "Unknown User");
} catch (error) {
console.error("Error fetching user:", error);
setSelectedUser("Error fetching user");
}
};

fetchSelectedUser();
}, []);

return (
<React.Fragment>
<Box sx={{ display: "flex", alignItems: "center", textAlign: "center" }}>
Expand All @@ -50,7 +73,7 @@ export default function AccountMenu() {
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
>
<Avatar sx={{ width: 32, height: 32 }}>M</Avatar>
<Avatar sx={{ width: 32, height: 32 }}>{selectedUser.charAt(0)}</Avatar>
</IconButton>
</Tooltip>
</Box>
Expand Down Expand Up @@ -99,7 +122,7 @@ export default function AccountMenu() {
color: "primary.main",
}}
>
<Avatar /> Profile
<Avatar /> {selectedUser}
</MenuItem>
<Divider />
<MenuItem onClick={handleClose} sx={{ fontFamily: "JetBrains Mono" }}>
Expand Down
33 changes: 28 additions & 5 deletions frontend/src/app/users/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,32 @@ const Users = () => {
setPasswordInput("");
setPasswordValid(null);
};

const handlePasswordSubmit = () => {
if (passwordInput === selectedUser.user_password) {
const handlePasswordSubmit = async () => {
if (selectedUser && passwordInput === selectedUser.user_password) {
setPasswordValid(true);
router.push("/dashboard");

try {
// Now, only set the selected user AFTER successful login
const response = await fetch(`http://localhost:8000/select_user/${selectedUser.user_name}`, {
method: "POST",
});

if (!response.ok) {
console.error("Failed to set selected user");
return;
}

// Fetch to verify if the user was actually set
const selectedResponse = await fetch("http://localhost:8000/selected_user");
const selectedData = await selectedResponse.json();
console.log("Selected user from API:", selectedData);

// Redirect to dashboard after successful login
router.push("/dashboard");
} catch (error) {
console.error("Error setting selected user:", error);
}
} else {
setPasswordValid(false);
}
Expand Down Expand Up @@ -236,7 +257,9 @@ const Users = () => {
)}

<Dialog open={openDialog} onClose={() => setOpenDialog(false)}>
<DialogTitle sx={{fontFamily: "JetBrains Mono", textAlign: "center"}}>Enter Password for {selectedUser?.user_name}</DialogTitle>
<DialogTitle>
Enter Password for {selectedUser ? selectedUser.user_name : "User"}
</DialogTitle>
<DialogContent>
<DotInput value={passwordInput} maxLength={4} />
<Typography variant="h6" sx={{ textAlign: "center", mt: 2, mb:2, fontFamily: "JetBrains Mono", fontSize: {xs: "15px", md:"24px"} }}>
Expand Down