## Summary What changed and why? ## Change Type - [ ] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: thanhnv <thanhnv.ip@gmail.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
"""Microsoft 365 sign-in dialog (DF-007) — thin UI over the working device-code
|
||||
flow in ``core/ms365_auth.py``. There was an older MS365 sign-in UI in this
|
||||
app; it was removed as dead code (no entry point — see
|
||||
``ui/settings_dialog.py`` module docstring) before this feature existed, so
|
||||
this is a fresh, small dialog rather than a resurrection of that one.
|
||||
|
||||
Usage: ``if ensure_signed_in(parent, ctx.config): ...`` — returns ``True``
|
||||
immediately (no dialog shown) when already signed in.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import webbrowser
|
||||
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QHBoxLayout, QLabel, QMessageBox, QPushButton, QVBoxLayout,
|
||||
)
|
||||
|
||||
from ..core import ms365_auth
|
||||
from ..i18n import tr
|
||||
|
||||
|
||||
class _SignInWorker(QThread):
|
||||
"""Chạy ``sign_in_device_code()`` (blocking, poll tới khi xong/hết hạn) ở
|
||||
luồng nền — xem ``core/worker.py::AgentWorker`` cho cùng idiom (bắt hết
|
||||
exception, phát signal thay vì để lỗi giết luồng âm thầm)."""
|
||||
code_ready = Signal(dict)
|
||||
finished_ok = Signal(dict)
|
||||
failed = Signal(str)
|
||||
|
||||
def __init__(self, config, parent=None):
|
||||
super().__init__(parent)
|
||||
self._config = config
|
||||
|
||||
def run(self) -> None: # noqa: D401
|
||||
try:
|
||||
result = ms365_auth.sign_in(lambda flow: self.code_ready.emit(flow), self._config)
|
||||
self.finished_ok.emit(result or {})
|
||||
except Exception as exc: # noqa: BLE001 - surfaced to the UI, never crashes the thread
|
||||
self.failed.emit(str(exc))
|
||||
|
||||
|
||||
class Ms365SignInDialog(QDialog):
|
||||
"""Modal: hiện user_code + verification_uri, tự mở trình duyệt, đóng lại
|
||||
khi đăng nhập xong (hoặc người dùng bấm Hủy)."""
|
||||
|
||||
def __init__(self, config, parent=None):
|
||||
super().__init__(parent)
|
||||
self._config = config
|
||||
self._worker: _SignInWorker | None = None
|
||||
self.setWindowTitle(tr("ms365_signin.title"))
|
||||
self.setModal(True)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self._intro_lbl = QLabel(tr("ms365_signin.intro"))
|
||||
self._intro_lbl.setWordWrap(True)
|
||||
layout.addWidget(self._intro_lbl)
|
||||
|
||||
self._code_lbl = QLabel()
|
||||
self._code_lbl.setWordWrap(True)
|
||||
self._code_lbl.hide()
|
||||
layout.addWidget(self._code_lbl)
|
||||
|
||||
self._open_link_btn = QPushButton(tr("ms365_signin.open_link"))
|
||||
self._open_link_btn.hide()
|
||||
self._open_link_btn.clicked.connect(self._open_link)
|
||||
layout.addWidget(self._open_link_btn)
|
||||
|
||||
self._error_lbl = QLabel()
|
||||
self._error_lbl.setWordWrap(True)
|
||||
self._error_lbl.setStyleSheet("color: #c0392b;")
|
||||
self._error_lbl.hide()
|
||||
layout.addWidget(self._error_lbl)
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
self._signin_btn = QPushButton(tr("ms365_signin.button"))
|
||||
self._signin_btn.setObjectName("primary")
|
||||
self._signin_btn.clicked.connect(self._start_sign_in)
|
||||
self._cancel_btn = QPushButton(tr("ms365_signin.cancel"))
|
||||
self._cancel_btn.clicked.connect(self.reject)
|
||||
btn_row.addStretch(1)
|
||||
btn_row.addWidget(self._cancel_btn)
|
||||
btn_row.addWidget(self._signin_btn)
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
self._verification_uri = ""
|
||||
|
||||
def _open_link(self) -> None:
|
||||
if self._verification_uri:
|
||||
webbrowser.open(self._verification_uri)
|
||||
|
||||
def _start_sign_in(self) -> None:
|
||||
self._signin_btn.setEnabled(False)
|
||||
self._signin_btn.setText(tr("ms365_signin.signing_in"))
|
||||
self._error_lbl.hide()
|
||||
self._worker = _SignInWorker(self._config, self)
|
||||
self._worker.code_ready.connect(self._on_code_ready)
|
||||
self._worker.finished_ok.connect(self._on_finished_ok)
|
||||
self._worker.failed.connect(self._on_failed)
|
||||
self._worker.start()
|
||||
|
||||
def _on_code_ready(self, flow: dict) -> None:
|
||||
self._verification_uri = flow.get("verification_uri_complete") or flow.get(
|
||||
"verification_uri", "")
|
||||
self._code_lbl.setText(
|
||||
tr("ms365_signin.code_hint", url=flow.get("verification_uri", "")) +
|
||||
f"\n\n{flow.get('user_code', '')}")
|
||||
self._code_lbl.show()
|
||||
self._open_link_btn.show()
|
||||
if self._verification_uri:
|
||||
webbrowser.open(self._verification_uri)
|
||||
|
||||
def _on_finished_ok(self, _result: dict) -> None:
|
||||
self.accept()
|
||||
|
||||
def _on_failed(self, err: str) -> None:
|
||||
self._signin_btn.setEnabled(True)
|
||||
self._signin_btn.setText(tr("ms365_signin.button"))
|
||||
self._error_lbl.setText(tr("ms365_signin.failed", err=err))
|
||||
self._error_lbl.show()
|
||||
|
||||
def reject(self) -> None:
|
||||
# NOTE: MSAL's acquire_token_by_device_flow() has no cancellation hook,
|
||||
# so a worker already polling keeps polling in the background until it
|
||||
# times out on its own (a few minutes) — closing this dialog just stops
|
||||
# the UI from waiting on it. Its late signals are harmless no-ops
|
||||
# against an already-closed (but not destroyed) dialog.
|
||||
super().reject()
|
||||
|
||||
|
||||
def ensure_signed_in(parent, config) -> bool:
|
||||
"""True nếu đã (hoặc vừa) đăng nhập MS365; False nếu người dùng hủy hoặc
|
||||
đăng nhập thất bại và đóng dialog."""
|
||||
if ms365_auth.is_signed_in(config):
|
||||
return True
|
||||
dialog = Ms365SignInDialog(config, parent)
|
||||
return dialog.exec() == QDialog.Accepted
|
||||
Reference in New Issue
Block a user