diff --git a/physiolabxr/__init__.py b/physiolabxr/__init__.py
index ffdb3d4f..88c75781 100644
--- a/physiolabxr/__init__.py
+++ b/physiolabxr/__init__.py
@@ -1,17 +1,21 @@
+import os
def physiolabxr():
import multiprocessing
import sys
+ import webbrowser
from PyQt6 import QtWidgets
from PyQt6.QtGui import QIcon
- from PyQt6.QtWidgets import QSystemTrayIcon, QMenu
+ from PyQt6.QtWidgets import QSystemTrayIcon, QMenu, QInputDialog, QMessageBox
+ from PyQt6.QtCore import QSettings
from physiolabxr.configs.configs import AppConfigs
from physiolabxr.configs.NetworkManager import NetworkManager
from physiolabxr.ui.SplashScreen import SplashScreen
-
+ from physiolabxr.ui.SplashScreen import SplashLoadingTextNotifier
+ from physiolabxr.ui.Login import LoginDialog
AppConfigs(_reset=False) # create the singleton app configs object
NetworkManager()
@@ -30,6 +34,18 @@ def physiolabxr():
splash = SplashScreen()
splash.show()
+ SplashLoadingTextNotifier().set_loading_text("Logging in...")
+
+ login_dialog = LoginDialog()
+
+ auto_login_success = login_dialog.auto_login()
+
+ if not auto_login_success:
+ if login_dialog.exec() != QtWidgets.QDialog.DialogCode.Accepted:
+ splash.close()
+ QMessageBox.critical(None, "Access Denied", "Login required to access the application.")
+ sys.exit()
+
# load default settings
from physiolabxr.utils.setup_utils import run_setup_check
run_setup_check()
@@ -56,4 +72,4 @@ def physiolabxr():
sys.exit(app.exec())
except KeyboardInterrupt:
print('App terminate by KeyboardInterrupt')
- sys.exit()
+ sys.exit()
\ No newline at end of file
diff --git a/physiolabxr/_ui/login.ui b/physiolabxr/_ui/login.ui
new file mode 100644
index 00000000..6121fa23
--- /dev/null
+++ b/physiolabxr/_ui/login.ui
@@ -0,0 +1,130 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 425
+ 358
+
+
+
+ Dialog
+
+
+
+
+ 130
+ 10
+ 191
+ 20
+
+
+
+ Welcome to PhysioLabXR!
+
+
+
+
+
+ 40
+ 150
+ 331
+ 41
+
+
+
+ Log In
+
+
+
+
+
+ 40
+ 270
+ 331
+ 41
+
+
+
+ Sign Up
+
+
+
+
+
+ 40
+ 70
+ 331
+ 21
+
+
+
+
+
+
+ 40
+ 50
+ 58
+ 16
+
+
+
+ Email:
+
+
+
+
+
+ 40
+ 100
+ 58
+ 16
+
+
+
+ Password:
+
+
+
+
+
+ 40
+ 120
+ 331
+ 21
+
+
+
+
+
+
+ 40
+ 240
+ 251
+ 16
+
+
+
+ Doesn't have an account with us yet?
+
+
+
+
+
+ 80
+ 200
+ 251
+ 20
+
+
+
+ Remember my account on this device
+
+
+
+
+
+
diff --git a/physiolabxr/_ui/mainwindow.ui b/physiolabxr/_ui/mainwindow.ui
index 1e2e0136..39f61305 100644
--- a/physiolabxr/_ui/mainwindow.ui
+++ b/physiolabxr/_ui/mainwindow.ui
@@ -62,8 +62,8 @@
0
0
- 1554
- 857
+ 1524
+ 810
@@ -293,7 +293,7 @@
0
0
1600
- 21
+ 37
+
+
@@ -346,6 +354,16 @@
Settings
+
+
+ Sign out
+
+
+
+
+ Sign Out
+
+
tabWidget
diff --git a/physiolabxr/configs/configs.py b/physiolabxr/configs/configs.py
index c75c89ef..805deec1 100644
--- a/physiolabxr/configs/configs.py
+++ b/physiolabxr/configs/configs.py
@@ -79,7 +79,7 @@ class AppConfigs(metaclass=Singleton):
_app_data_name: str = 'RenaLabApp'
app_data_path = os.path.join(QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation), _app_data_name)
-
+ print("⚠️" + app_data_path)
# appearance configs
theme: str = 'dark' # TODO: refactor this to an enum, replace config.value
@@ -130,6 +130,10 @@ class AppConfigs(metaclass=Singleton):
# Tobii Pro Fusion Eye Tracker Manager
tobii_app_path: str = None
+ # user login
+ remembered_token: str = None
+ refresh_token: str = None
+
_media_paths = ['physiolabxr/_media/icons', 'physiolabxr/_media/logo', 'physiolabxr/_media/gifs']
_supported_media_formats = ['.svg', '.gif']
_ui_path = 'physiolabxr/_ui'
diff --git a/physiolabxr/ui/Login.py b/physiolabxr/ui/Login.py
new file mode 100644
index 00000000..3368b829
--- /dev/null
+++ b/physiolabxr/ui/Login.py
@@ -0,0 +1,193 @@
+import os
+import sys
+import webbrowser
+import requests
+from PyQt6.QtCore import Qt
+from PyQt6.QtGui import QFont
+from dotenv import load_dotenv
+import firebase_admin
+from firebase_admin import auth
+from PyQt6 import QtCore, QtWidgets
+from PyQt6.QtWidgets import QDialog, QMessageBox
+from physiolabxr.configs.configs import AppConfigs
+
+
+FIREBASE_API_KEY = "AIzaSyD7CJXqoCPtv2GzMQpGLKwTo4MacPqjqnw"
+
+class LoginDialog(QDialog):
+ def __init__(self):
+ super().__init__()
+ self.setupUi()
+
+
+ def setupUi(self):
+ self.setObjectName("Dialog")
+ self.resize(425, 358)
+
+ self.label = QtWidgets.QLabel(self)
+ self.label.setGeometry(QtCore.QRect(50, 10, 191, 20))
+ self.label.setObjectName("label")
+
+ self.pushButton = QtWidgets.QPushButton(self)
+ self.pushButton.setGeometry(QtCore.QRect(40, 150, 331, 41))
+ self.pushButton.setObjectName("pushButton")
+
+ self.pushButton_2 = QtWidgets.QPushButton(self)
+ self.pushButton_2.setGeometry(QtCore.QRect(40, 270, 331, 41))
+ self.pushButton_2.setObjectName("pushButton_2")
+
+ self.lineEdit = QtWidgets.QLineEdit(self)
+ self.lineEdit.setGeometry(QtCore.QRect(40, 70, 331, 21))
+ self.lineEdit.setObjectName("lineEdit")
+
+ self.label_2 = QtWidgets.QLabel(self)
+ self.label_2.setGeometry(QtCore.QRect(40, 50, 58, 16))
+ self.label_2.setObjectName("label_2")
+
+ self.label_3 = QtWidgets.QLabel(self)
+ self.label_3.setGeometry(QtCore.QRect(40, 100, 58, 16))
+ self.label_3.setObjectName("label_3")
+
+ self.lineEdit_2 = QtWidgets.QLineEdit(self)
+ self.lineEdit_2.setGeometry(QtCore.QRect(40, 120, 331, 21))
+ self.lineEdit_2.setObjectName("lineEdit_2")
+ self.lineEdit_2.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password) # Hide password input
+
+ self.label_4 = QtWidgets.QLabel(self)
+ self.label_4.setGeometry(QtCore.QRect(40, 240, 251, 16))
+ self.label_4.setObjectName("label_4")
+
+ self.checkBox = QtWidgets.QCheckBox(self)
+ self.checkBox.setGeometry(QtCore.QRect(80, 200, 251, 20))
+ self.checkBox.setObjectName("checkBox")
+
+ self.retranslateUi()
+ self.pushButton.clicked.connect(self.handle_login)
+ self.pushButton_2.clicked.connect(self.handle_signup)
+
+ def retranslateUi(self):
+ self.setWindowTitle("Welcome")
+ self.label.setText("Log into your PhysioLabXR account")
+ font = QFont()
+ font.setBold(True) # Make the text bold
+ font.setPointSize(20) # Set the font size
+ self.label.setFont(font)
+ self.label.adjustSize()
+
+ self.pushButton.setText("Log In")
+ self.pushButton_2.setText("Sign Up")
+ self.label_2.setText("Email:")
+ self.label_3.setText("Password:")
+ self.label_4.setText("Don't have an account yet?")
+ self.checkBox.setText("Remember my account on this device")
+
+ def handle_login(self):
+ """Handle user login using Firebase Authentication & Store Refresh Token."""
+ email = self.lineEdit.text()
+ password = self.lineEdit_2.text()
+
+ if not email or not password:
+ QMessageBox.warning(self, "Error", "Please enter both email and password.")
+ return
+
+ try:
+ url = f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={FIREBASE_API_KEY}"
+ data = {"email": email, "password": password, "returnSecureToken": True}
+ response = requests.post(url, json=data)
+ response_data = response.json()
+
+ if "idToken" in response_data:
+ id_token = response_data["idToken"]
+ refresh_token = response_data["refreshToken"]
+ local_id = response_data["localId"]
+
+ if self.checkBox.isChecked():
+ AppConfigs().remembered_token = id_token
+ AppConfigs().refresh_token = refresh_token
+
+ print(f"✅ Login Successful - User: {local_id}")
+
+ self.accept() # Close the dialog on success
+
+ else:
+ error_message = response_data.get("error", {}).get("message", "Unknown error occurred.")
+ QMessageBox.critical(self, "Login Failed", f"An error occurred: {error_message}")
+
+ except Exception as e:
+ QMessageBox.critical(self, "Error", f"An unexpected error occurred: {e}")
+
+ def refresh_id_token(self):
+ """Refresh Firebase ID Token using the refresh token."""
+ refresh_token = AppConfigs().refresh_token
+
+ if not refresh_token:
+ print("❌ No refresh token found, user needs to log in again.")
+ return None
+
+ try:
+ url = f"https://securetoken.googleapis.com/v1/token?key={FIREBASE_API_KEY}"
+ data = {"grant_type": "refresh_token", "refresh_token": refresh_token}
+ response = requests.post(url, json=data)
+ response_data = response.json()
+
+ if "id_token" in response_data:
+ new_id_token = response_data["id_token"]
+ new_refresh_token = response_data["refresh_token"]
+
+ # ✅ Update stored tokens
+ AppConfigs().remembered_token = new_id_token
+ AppConfigs().refresh_token = new_refresh_token
+
+ print("🔄 Token refreshed successfully")
+ return new_id_token
+
+ else:
+ print("❌ Failed to refresh token")
+ return None
+
+ except Exception as e:
+ QMessageBox.critical(self, "Token Refresh Failed", f"An unexpected error occurred: {e}")
+ return None
+
+ def auto_login(self):
+ """Auto-login using stored and refreshed token."""
+ id_token = self.refresh_id_token() or AppConfigs().remembered_token
+
+ if not id_token:
+ print("❌ No valid token found, requiring manual login.")
+ return False
+
+ try:
+ url = f"https://identitytoolkit.googleapis.com/v1/accounts:lookup?key={FIREBASE_API_KEY}"
+ data = {"idToken": id_token}
+ response = requests.post(url, json=data)
+
+ if response.status_code == 200:
+ print("✅ Auto-login successful")
+ self.accept()
+ return True
+ else:
+ print("❌ Invalid token, requiring manual login.")
+ AppConfigs().remembered_token = None
+ return False
+
+ except Exception as e:
+ QMessageBox.critical(self, "Auto Login Failed", f"An unexpected error occurred: {e}")
+ return False
+
+ def handle_signup(self):
+ """Redirect user to the signup webpage."""
+ signup_url = "https://storage.googleapis.com/physiolabxr.org/signup.html"
+ webbrowser.open(signup_url)
+ QMessageBox.information(self, "Redirecting", "You will be redirected to the signup page.")
+
+if __name__ == "__main__":
+ # Initialize Firebase Admin SDK if not already initialized
+ if not firebase_admin._apps:
+ cred = firebase_admin.credentials.Certificate(
+ "path/to/serviceAccountKey.json") # Replace with your Google credentials path
+ firebase_admin.initialize_app(cred)
+
+ app = QtWidgets.QApplication(sys.argv)
+ dialog = LoginDialog()
+ dialog.exec()
diff --git a/physiolabxr/ui/MainWindow.py b/physiolabxr/ui/MainWindow.py
index 16adfa67..82c49a97 100644
--- a/physiolabxr/ui/MainWindow.py
+++ b/physiolabxr/ui/MainWindow.py
@@ -44,6 +44,8 @@
import numpy as np
+from firebase_admin import auth
+
# Define function to import external files when using PyInstaller.
def resource_path(relative_path):
@@ -127,6 +129,7 @@ def __init__(self, app, ask_to_close=True, *args, **kwargs):
self.actionShow_Recordings.triggered.connect(self.fire_action_show_recordings)
self.actionExit.triggered.connect(self.fire_action_exit)
self.actionSettings.triggered.connect(self.fire_action_settings)
+ self.actionSign_Out.triggered.connect(self.on_sign_out_triggered)
# create the settings window
self.settings_widget = SettingsWidget(self)
@@ -518,4 +521,36 @@ def resizeEvent(self, a0):
self.adjust_notification_panel_location()
def adjust_notification_panel_location(self):
- self.notification_panel.move(self.width() - self.notification_panel.width() - 9, self.height() - self.notification_panel.height() - self.recording_file_size_label.height() - 12) # substract 64 to account for margin
\ No newline at end of file
+ self.notification_panel.move(self.width() - self.notification_panel.width() - 9, self.height() - self.notification_panel.height() - self.recording_file_size_label.height() - 12) # substract 64 to account for margin
+
+ def on_sign_out_triggered(self):
+ """Handles user sign-out, clears remembered user data, and quits the app."""
+ reply = QtWidgets.QMessageBox.question(
+ self,
+ 'Sign Out',
+ 'Signing out will close the app and remove your saved account login information. Are you sure?',
+ QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
+ QtWidgets.QMessageBox.StandardButton.No
+ )
+
+ if reply == QtWidgets.QMessageBox.StandardButton.Yes:
+ try:
+ # Get the current remembered user UID
+ remembered_uid = AppConfigs().remembered_uid
+
+ if remembered_uid:
+ # Revoke Firebase session (Optional: Only if using Firebase Admin)
+ auth.revoke_refresh_tokens(remembered_uid)
+
+ # Clear remembered user data
+ AppConfigs().remembered_token = None
+ AppConfigs().refresh_token = None
+
+ # Display message
+ QMessageBox.information(self, "Signed Out", "You have been signed out.")
+
+ # Close the app
+ self.ask_to_close = False
+ self.close()
+ except Exception as e:
+ QMessageBox.critical(self, "Sign Out Failed", f"Error while signing out: {e}")
diff --git a/requirements.dev.txt b/requirements.dev.txt
index 2678e44f..11c9a738 100644
--- a/requirements.dev.txt
+++ b/requirements.dev.txt
@@ -22,3 +22,5 @@ imblearn
toml
grpcio
grpcio-tools
+python-dotenv
+firebase-admin
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 010c90b3..c5bbc046 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,10 +14,11 @@ setuptools
psutil
numba
PyOpenGL
-PyOpenGL_accelerate
soundfile
matplotlib
imblearn
toml
grpcio
-grpcio-tools
\ No newline at end of file
+grpcio-tools
+python-dotenv
+firebase-admin
\ No newline at end of file