"""Studio login window (PySide6). Lists every active user from the local store, lets the operator pick themselves and enter their PIN. On success, populates the global :class:`SessionHolder` and emits :pyattr:`logged_in`. """ from __future__ import annotations from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( QComboBox, QDialog, QDialogButtonBox, QFormLayout, QLabel, QLineEdit, QMessageBox, QVBoxLayout, ) from arautopilot.core.audit import AuditEvent, AuditLog, AuditOutcome from arautopilot.core.user_store import UserStore from arautopilot.studio.session import Session, SessionHolder class LoginDialog(QDialog): """Modal login dialog. Emits :pyattr:`logged_in(session)` on success.""" logged_in = Signal(object) # Session def __init__( self, store: UserStore, audit: AuditLog, parent: object | None = None, ) -> None: super().__init__(parent) self._store = store self._audit = audit self.setWindowTitle("AR-Autopilot Studio -- Login") self.setModal(True) self.setMinimumWidth(360) layout = QVBoxLayout(self) intro = QLabel("Select your user and enter the PIN.") intro.setWordWrap(True) layout.addWidget(intro) form = QFormLayout() self._user_combo = QComboBox() for user in self._store.all_users(): if not user.active: continue self._user_combo.addItem( f"{user.display_name} -- {user.role.value}", userData=user.user_id, ) form.addRow("User:", self._user_combo) self._pin_field = QLineEdit() self._pin_field.setEchoMode(QLineEdit.EchoMode.Password) self._pin_field.setMaxLength(8) self._pin_field.setPlaceholderText("4-8 digits") self._pin_field.setInputMethodHints(Qt.InputMethodHint.ImhDigitsOnly) form.addRow("PIN:", self._pin_field) layout.addLayout(form) buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) buttons.accepted.connect(self._try_login) buttons.rejected.connect(self.reject) layout.addWidget(buttons) if self._user_combo.count() == 0: QMessageBox.critical( self, "No users available", "The local user store is empty. Run the demo seed:\n\n" " python -m arautopilot.studio.app --seed-demo", ) # ----- Internal ------------------------------------------------------- def _try_login(self) -> None: user_id = self._user_combo.currentData() pin = self._pin_field.text() user = self._store.get(user_id) if user_id else None if user is None: QMessageBox.warning(self, "Login", "Pick a user.") return if not user.active: QMessageBox.warning(self, "Login", "This user is disabled.") return if not user.verify_pin(pin): self._audit.append( AuditEvent( user_id=user.user_id, role=user.role.value, action="login", outcome=AuditOutcome.DENIED, reason="bad PIN", ) ) QMessageBox.warning(self, "Login", "Incorrect PIN.") self._pin_field.clear() self._pin_field.setFocus() return touched = user.touch_login() self._store.replace(touched) self._audit.append( AuditEvent( user_id=touched.user_id, role=touched.role.value, action="login", outcome=AuditOutcome.SUCCESS, ) ) session = Session(user=touched, store=self._store, audit=self._audit) SessionHolder.set(session) self.logged_in.emit(session) self.accept()