mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 00:17:58 +02:00
399 lines
15 KiB
Python
399 lines
15 KiB
Python
"""Nexus authentication methods for InstallModlistScreen (Mixin)."""
|
|
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox, QApplication
|
|
from PySide6.QtCore import Qt, QThread, Signal
|
|
from PySide6.QtGui import QDesktopServices, QGuiApplication
|
|
import logging
|
|
import webbrowser
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class NexusAuthMixin:
|
|
"""Mixin providing Nexus authentication methods for InstallModlistScreen."""
|
|
|
|
def _update_nexus_status(self):
|
|
"""Update the Nexus login status display"""
|
|
authenticated, method, username = self.auth_service.get_auth_status()
|
|
|
|
if authenticated and method == 'oauth':
|
|
# OAuth authorised
|
|
status_text = "Authorised"
|
|
if username:
|
|
status_text += f" ({username})"
|
|
self.nexus_status.setText(status_text)
|
|
self.nexus_status.setStyleSheet("color: #3fd0ea;")
|
|
self.nexus_login_btn.setText("Revoke")
|
|
self.nexus_login_btn.setVisible(True)
|
|
elif authenticated and method == 'api_key':
|
|
# API Key in use (fallback - configured in Settings)
|
|
self.nexus_status.setText("API Key")
|
|
self.nexus_status.setStyleSheet("color: #FFA726;")
|
|
self.nexus_login_btn.setText("Authorise")
|
|
self.nexus_login_btn.setVisible(True)
|
|
else:
|
|
# Not authorised
|
|
self.nexus_status.setText("Not Authorised")
|
|
self.nexus_status.setStyleSheet("color: #f44336;")
|
|
self.nexus_login_btn.setText("Authorise")
|
|
self.nexus_login_btn.setVisible(True)
|
|
|
|
def _show_copyable_url_dialog(self, url: str):
|
|
"""Show a dialog with a copyable URL"""
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle("Manual Browser Open Required")
|
|
dialog.setModal(True)
|
|
dialog.setMinimumWidth(600)
|
|
|
|
layout = QVBoxLayout()
|
|
layout.setSpacing(15)
|
|
|
|
info_label = QLabel(
|
|
"Could not open browser automatically.\n\n"
|
|
"Please copy the URL below and paste it into your browser:"
|
|
)
|
|
info_label.setWordWrap(True)
|
|
info_label.setStyleSheet("color: #ccc; font-size: 12px;")
|
|
layout.addWidget(info_label)
|
|
|
|
url_input = QLineEdit()
|
|
url_input.setText(url)
|
|
url_input.setReadOnly(True)
|
|
url_input.selectAll()
|
|
url_input.setStyleSheet("""
|
|
QLineEdit {
|
|
background-color: #1a1a1a;
|
|
color: #3fd0ea;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
font-family: monospace;
|
|
font-size: 11px;
|
|
}
|
|
""")
|
|
layout.addWidget(url_input)
|
|
|
|
button_layout = QHBoxLayout()
|
|
button_layout.addStretch()
|
|
|
|
copy_btn = QPushButton("Copy URL")
|
|
copy_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #3fd0ea;
|
|
color: #000;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 8px 20px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #5fdfff;
|
|
}
|
|
""")
|
|
def copy_to_clipboard():
|
|
clipboard = QApplication.clipboard()
|
|
clipboard.setText(url)
|
|
copy_btn.setText("Copied!")
|
|
copy_btn.setEnabled(False)
|
|
copy_btn.clicked.connect(copy_to_clipboard)
|
|
button_layout.addWidget(copy_btn)
|
|
|
|
close_btn = QPushButton("Close")
|
|
close_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #444;
|
|
color: #ccc;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 8px 20px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #555;
|
|
}
|
|
""")
|
|
close_btn.clicked.connect(dialog.accept)
|
|
button_layout.addWidget(close_btn)
|
|
|
|
layout.addLayout(button_layout)
|
|
dialog.setLayout(layout)
|
|
dialog.exec()
|
|
|
|
def _show_oauth_paste_dialog(self):
|
|
"""Show dialog for pasting jackify:// callback URL as manual fallback."""
|
|
import urllib.parse
|
|
from pathlib import Path
|
|
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle("Paste Callback URL")
|
|
dialog.setModal(True)
|
|
dialog.setMinimumWidth(560)
|
|
|
|
layout = QVBoxLayout()
|
|
layout.setSpacing(12)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
|
|
info_label = QLabel(
|
|
"If your browser did not complete the flow automatically:\n\n"
|
|
"1. Click Continue in your browser if you have not already.\n"
|
|
"2. If a URL starting with jackify:// appears in your browser\n"
|
|
" address bar, copy it and paste it below."
|
|
)
|
|
info_label.setWordWrap(True)
|
|
info_label.setStyleSheet("color: #ccc; font-size: 12px;")
|
|
layout.addWidget(info_label)
|
|
|
|
url_input = QLineEdit()
|
|
url_input.setPlaceholderText("jackify://oauth/callback?code=...&state=...")
|
|
url_input.setStyleSheet("""
|
|
QLineEdit {
|
|
background-color: #1a1a1a;
|
|
color: #3fd0ea;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
font-family: monospace;
|
|
font-size: 11px;
|
|
}
|
|
""")
|
|
layout.addWidget(url_input)
|
|
|
|
error_label = QLabel("")
|
|
error_label.setStyleSheet("color: #f44336; font-size: 11px;")
|
|
error_label.setWordWrap(True)
|
|
layout.addWidget(error_label)
|
|
|
|
btn_layout = QHBoxLayout()
|
|
btn_layout.addStretch()
|
|
|
|
submit_btn = QPushButton("Submit")
|
|
submit_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #3fd0ea;
|
|
color: #000;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 8px 20px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #5fdfff;
|
|
}
|
|
""")
|
|
|
|
def on_submit():
|
|
url = url_input.text().strip()
|
|
if not url.startswith('jackify://oauth/callback'):
|
|
error_label.setText("URL must start with jackify://oauth/callback")
|
|
return
|
|
parsed = urllib.parse.urlparse(url)
|
|
params = urllib.parse.parse_qs(parsed.query)
|
|
code = params.get('code', [None])[0]
|
|
state = params.get('state', [None])[0]
|
|
if not code or not state:
|
|
error_label.setText("URL is missing required code or state parameter.")
|
|
return
|
|
callback_file = Path.home() / ".config" / "jackify" / "oauth_callback.tmp"
|
|
try:
|
|
callback_file.parent.mkdir(parents=True, exist_ok=True)
|
|
callback_file.write_text(f"{code}\n{state}")
|
|
logger.info("OAuth callback written via manual paste")
|
|
dialog.accept()
|
|
except Exception as e:
|
|
error_label.setText(f"Failed to write callback: {e}")
|
|
|
|
submit_btn.clicked.connect(on_submit)
|
|
url_input.returnPressed.connect(on_submit)
|
|
btn_layout.addWidget(submit_btn)
|
|
|
|
cancel_btn = QPushButton("Cancel")
|
|
cancel_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #444;
|
|
color: #ccc;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 8px 20px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #555;
|
|
}
|
|
""")
|
|
cancel_btn.clicked.connect(dialog.reject)
|
|
btn_layout.addWidget(cancel_btn)
|
|
|
|
layout.addLayout(btn_layout)
|
|
dialog.setLayout(layout)
|
|
dialog.exec()
|
|
|
|
def _handle_nexus_login_click(self):
|
|
"""Handle Nexus login button click"""
|
|
from jackify.frontends.gui.services.message_service import MessageService
|
|
|
|
authenticated, method, _ = self.auth_service.get_auth_status()
|
|
if authenticated and method == 'oauth':
|
|
reply = MessageService.question(self, "Revoke", "Revoke OAuth authorisation?", safety_level="low")
|
|
if reply == QMessageBox.Yes:
|
|
self.auth_service.revoke_oauth()
|
|
self._update_nexus_status()
|
|
else:
|
|
reply = MessageService.question(self, "Authorise with Nexus",
|
|
"Your browser will open for Nexus authorisation.\n\n"
|
|
"Note: Your browser may ask permission to open 'xdg-open'\n"
|
|
"or Jackify's protocol handler - please click 'Open' or 'Allow'.\n\n"
|
|
"Please log in and authorise Jackify when prompted.\n\n"
|
|
"Continue?", safety_level="low")
|
|
|
|
if reply != QMessageBox.Yes:
|
|
return
|
|
|
|
# Build waiting dialog with paste fallback always accessible
|
|
wait_dialog = QDialog(self)
|
|
wait_dialog.setWindowTitle("Nexus OAuth")
|
|
wait_dialog.setWindowModality(Qt.WindowModal)
|
|
wait_dialog.setMinimumWidth(420)
|
|
|
|
wait_layout = QVBoxLayout()
|
|
wait_layout.setSpacing(12)
|
|
wait_layout.setContentsMargins(20, 20, 20, 20)
|
|
|
|
wait_label = QLabel(
|
|
"Waiting for authorisation...\n\n"
|
|
"Please complete authorisation in your browser.\n\n"
|
|
"Your browser may ask permission to open Jackify — click Open or Allow."
|
|
)
|
|
wait_label.setWordWrap(True)
|
|
wait_label.setStyleSheet("color: #ccc; font-size: 12px;")
|
|
wait_layout.addWidget(wait_label)
|
|
|
|
wait_layout.addStretch()
|
|
|
|
btn_layout = QHBoxLayout()
|
|
|
|
paste_btn = QPushButton("Paste callback URL")
|
|
paste_btn.setToolTip(
|
|
"If your browser shows a jackify:// URL after clicking Continue, paste it here."
|
|
)
|
|
paste_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #333;
|
|
color: #aaa;
|
|
border: 1px solid #555;
|
|
border-radius: 4px;
|
|
padding: 8px 16px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #444;
|
|
color: #ccc;
|
|
}
|
|
""")
|
|
paste_btn.clicked.connect(self._show_oauth_paste_dialog)
|
|
btn_layout.addWidget(paste_btn)
|
|
|
|
btn_layout.addStretch()
|
|
|
|
oauth_cancelled = [False]
|
|
|
|
cancel_btn = QPushButton("Cancel")
|
|
cancel_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #444;
|
|
color: #ccc;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 8px 16px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #555;
|
|
}
|
|
""")
|
|
def on_cancel_click():
|
|
oauth_cancelled[0] = True
|
|
wait_dialog.close()
|
|
cancel_btn.clicked.connect(on_cancel_click)
|
|
btn_layout.addWidget(cancel_btn)
|
|
|
|
wait_layout.addLayout(btn_layout)
|
|
wait_dialog.setLayout(wait_layout)
|
|
wait_dialog.show()
|
|
QApplication.processEvents()
|
|
|
|
# Create OAuth thread to prevent GUI freeze
|
|
class OAuthThread(QThread):
|
|
finished_signal = Signal(bool)
|
|
message_signal = Signal(str)
|
|
manual_url_signal = Signal(str)
|
|
|
|
def __init__(self, auth_service, parent=None):
|
|
super().__init__(parent)
|
|
self.auth_service = auth_service
|
|
|
|
def run(self):
|
|
def show_message(msg):
|
|
if "Could not open browser" in msg and "Please open this URL manually:" in msg:
|
|
url_start = msg.find("Please open this URL manually:") + len("Please open this URL manually:")
|
|
url = msg[url_start:].strip()
|
|
self.manual_url_signal.emit(url)
|
|
else:
|
|
self.message_signal.emit(msg)
|
|
|
|
success = self.auth_service.authorize_oauth(show_browser_message_callback=show_message)
|
|
self.finished_signal.emit(success)
|
|
|
|
oauth_thread = OAuthThread(self.auth_service, self)
|
|
|
|
def update_progress_message(msg):
|
|
if not oauth_cancelled[0]:
|
|
wait_label.setText(f"Waiting for authorisation...\n\n{msg}")
|
|
QApplication.processEvents()
|
|
|
|
def show_manual_url_dialog(url):
|
|
if not oauth_cancelled[0]:
|
|
wait_dialog.hide()
|
|
self._show_copyable_url_dialog(url)
|
|
wait_dialog.show()
|
|
|
|
oauth_thread.message_signal.connect(update_progress_message)
|
|
oauth_thread.manual_url_signal.connect(show_manual_url_dialog)
|
|
|
|
oauth_success = [False]
|
|
def on_oauth_finished(success):
|
|
oauth_success[0] = success
|
|
|
|
oauth_thread.finished_signal.connect(on_oauth_finished)
|
|
oauth_thread.start()
|
|
|
|
while oauth_thread.isRunning():
|
|
QApplication.processEvents()
|
|
oauth_thread.wait(100)
|
|
if oauth_cancelled[0]:
|
|
oauth_thread.wait(2000)
|
|
if oauth_thread.isRunning():
|
|
oauth_thread.terminate()
|
|
break
|
|
|
|
wait_dialog.close()
|
|
QApplication.processEvents()
|
|
|
|
self._update_nexus_status()
|
|
self._enable_controls_after_operation()
|
|
|
|
if oauth_success[0]:
|
|
_, _, username = self.auth_service.get_auth_status()
|
|
if username:
|
|
msg = f"OAuth authorisation successful!<br><br>Authorised as: {username}"
|
|
else:
|
|
msg = "OAuth authorisation successful!"
|
|
MessageService.information(self, "Success", msg, safety_level="low")
|
|
elif oauth_cancelled[0]:
|
|
MessageService.information(self, "Cancelled", "OAuth authorisation cancelled.", safety_level="low")
|
|
else:
|
|
MessageService.warning(
|
|
self,
|
|
"Authorisation Failed",
|
|
"OAuth authorisation timed out.\n\n"
|
|
"If your browser shows a URL starting with jackify:// after\n"
|
|
"clicking Continue, try again and use 'Paste callback URL'\n"
|
|
"during the wait to complete authorisation manually.\n\n"
|
|
"If the issue persists, an API key can be configured in Settings.",
|
|
safety_level="medium"
|
|
)
|