forked from Support/sp-hydra-veil-gui
2189 lines
89 KiB
Python
Executable file
2189 lines
89 KiB
Python
Executable file
import logging
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
from datetime import datetime, timezone
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QApplication, QButtonGroup, QCheckBox, QComboBox, QFrame, QGridLayout,
|
|
QGroupBox, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPushButton,
|
|
QScrollArea, QStackedLayout, QVBoxLayout, QWidget,
|
|
)
|
|
from PyQt6.QtGui import QIcon
|
|
from PyQt6.QtCore import Qt, QSize
|
|
|
|
from core.Constants import Constants
|
|
from core.controllers.ConfigurationController import ConfigurationController
|
|
from core.controllers.PolicyController import PolicyController
|
|
from core.controllers.ProfileController import ProfileController
|
|
from core.controllers.tickets.UseTicketController import (
|
|
do_we_use_a_random_ticket,
|
|
get_unused_tickets,
|
|
modify_random_tickets_setting,
|
|
)
|
|
from core.models.session.SessionProfile import SessionProfile
|
|
from core.models.system.SystemProfile import SystemProfile
|
|
from core.Errors import (
|
|
CommandNotFoundError,
|
|
PolicyAssignmentError,
|
|
PolicyInstatementError,
|
|
PolicyRevocationError,
|
|
)
|
|
from core.errors.logger import logger as core_logger
|
|
|
|
from gui.v2.actions import settings_data
|
|
from gui.v2.actions.key_interpretation import interpret_key_results
|
|
from gui.v2.actions.profile_order import normalize_profile_order
|
|
from gui.v2.infrastructure.page_registry import PAGE_REGISTRY
|
|
from gui.v2.infrastructure.setup_observers import ticket_observer
|
|
from gui.v2.ui.pages.Page import Page
|
|
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
|
|
from gui.v2.ui.styles.styles import (
|
|
SCROLLBAR_CYAN_QSS,
|
|
TERMINAL_LIST_QSS,
|
|
checkbox_style,
|
|
combobox_style,
|
|
)
|
|
from gui.v2.ui.widgets.clickable_label import ClickableValueLabel
|
|
from gui.v2.ui.widgets.terminal_widget import TerminalWidget
|
|
from gui.v2.workers.page_data_worker import PageDataWorker
|
|
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
|
|
from gui.v2.workers.worker_thread import WorkerThread
|
|
|
|
|
|
class Settings(Page):
|
|
def __init__(self, page_stack, main_window, parent=None, prepared=None):
|
|
super().__init__("Settings", page_stack, main_window, parent)
|
|
self.font_style = f"font-family: '{self.custom_window.open_sans_family}';"
|
|
|
|
self.update_status = main_window
|
|
self.update_logging = main_window
|
|
self._prepared = prepared if prepared is not None else settings_data.empty_payload()
|
|
self._settings_refresh_seq = 0
|
|
self._settings_workers = set()
|
|
self.button_reverse.setVisible(True)
|
|
self.button_reverse.setEnabled(True)
|
|
self.button_reverse.clicked.connect(self.reverse)
|
|
self.title.setGeometry(585, 40, 185, 40)
|
|
self.title.setText("Settings")
|
|
self.setup_ui()
|
|
|
|
@staticmethod
|
|
def prepare_data(custom_window):
|
|
return settings_data.prepare(custom_window.gui_config_file)
|
|
|
|
def setup_ui(self):
|
|
main_container = QWidget(self)
|
|
main_container.setGeometry(0, 0, 800, 520)
|
|
|
|
self.layout = QHBoxLayout(main_container)
|
|
self.layout.setContentsMargins(0, 60, 0, 0)
|
|
|
|
self.left_panel = QWidget()
|
|
self.left_panel.setFixedWidth(200)
|
|
self.left_layout = QVBoxLayout(self.left_panel)
|
|
self.left_layout.setContentsMargins(0, 0, 0, 0)
|
|
self.left_layout.setSpacing(0)
|
|
|
|
self.content_widget = QWidget()
|
|
self.content_layout = QStackedLayout(self.content_widget)
|
|
|
|
self.layout.addWidget(self.left_panel)
|
|
self.layout.addWidget(self.content_widget)
|
|
|
|
self.setup_menu_buttons()
|
|
self.setup_pages()
|
|
|
|
def setup_menu_buttons(self):
|
|
menu_items = [
|
|
("Overview", self.show_account_page),
|
|
("Subscriptions", self.show_subscription_page),
|
|
("Tickets", self.show_tickets_page),
|
|
("Create/Edit", self.show_registrations_page),
|
|
("Verification", self.show_verification_page),
|
|
("Legacy-Version", self.show_systemwide_page),
|
|
("Bwrap Permission", self.show_bwrap_page),
|
|
("Delete Profile", self.show_delete_page),
|
|
("Error Logs", self.show_logs_page),
|
|
("Debug Help", self.show_debug_page)
|
|
]
|
|
|
|
self.menu_buttons = []
|
|
for text, callback in menu_items:
|
|
btn = QPushButton(text)
|
|
btn.setFixedHeight(40)
|
|
btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
btn.setCheckable(True)
|
|
btn.clicked.connect(callback)
|
|
|
|
btn.setStyleSheet(f"""
|
|
QPushButton {{
|
|
text-align: left;
|
|
padding-left: 20px;
|
|
border: none;
|
|
background: transparent;
|
|
color: #808080;
|
|
font-size: 14px;
|
|
{self.font_style}
|
|
}}
|
|
QPushButton:checked {{
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: white;
|
|
border-left: 3px solid #007AFF;
|
|
}}
|
|
QPushButton:hover:!checked {{
|
|
background: rgba(255, 255, 255, 0.05);
|
|
}}
|
|
""")
|
|
|
|
self.left_layout.addWidget(btn)
|
|
self.menu_buttons.append(btn)
|
|
|
|
self.left_layout.addStretch()
|
|
|
|
def create_delete_page(self):
|
|
page = QWidget()
|
|
layout = QVBoxLayout(page)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
layout.setSpacing(15)
|
|
|
|
title = QLabel("DELETE PROFILE")
|
|
title.setStyleSheet(
|
|
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
layout.addWidget(title)
|
|
|
|
self.delete_scroll_area = QScrollArea()
|
|
self.delete_scroll_area.setFixedSize(510, 300)
|
|
self.delete_scroll_area.setWidgetResizable(True)
|
|
self.delete_scroll_area.setHorizontalScrollBarPolicy(
|
|
Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
self.delete_scroll_area.setVerticalScrollBarPolicy(
|
|
Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
self.delete_scroll_area.setStyleSheet(SCROLLBAR_CYAN_QSS)
|
|
|
|
self.delete_scroll_widget = QWidget()
|
|
self.delete_scroll_widget.setStyleSheet(
|
|
"background-color: transparent;")
|
|
self.delete_scroll_area.setWidget(self.delete_scroll_widget)
|
|
|
|
self.profile_buttons = QButtonGroup()
|
|
self.profile_buttons.setExclusive(True)
|
|
|
|
self.create_delete_profile_buttons()
|
|
|
|
layout.addWidget(self.delete_scroll_area)
|
|
|
|
self.delete_button = QPushButton("Delete")
|
|
self.delete_button.setEnabled(False)
|
|
self.delete_button.setFixedSize(120, 40)
|
|
self.delete_button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background: #ff4d4d;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
font-weight: bold;
|
|
{self.font_style}
|
|
}}
|
|
QPushButton:disabled {{
|
|
background: #666666;
|
|
}}
|
|
QPushButton:hover:!disabled {{
|
|
background: #ff3333;
|
|
}}
|
|
""")
|
|
self.delete_button.clicked.connect(self.delete_selected_profile)
|
|
self.profile_buttons.buttonClicked.connect(self.on_profile_selected)
|
|
|
|
button_layout = QHBoxLayout()
|
|
button_layout.addStretch()
|
|
button_layout.addWidget(self.delete_button)
|
|
button_layout.addStretch()
|
|
|
|
layout.addStretch()
|
|
layout.addLayout(button_layout)
|
|
|
|
return page
|
|
|
|
def create_delete_profile_buttons(self):
|
|
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
|
|
|
profile_ids = self._prepared_value(
|
|
"profile_order",
|
|
lambda: normalize_profile_order(
|
|
getattr(self.update_status, 'gui_config_file', None),
|
|
profiles.keys()))
|
|
|
|
for index, profile_id in enumerate(profile_ids):
|
|
profile = profiles[profile_id]
|
|
row = index // 2
|
|
col = index % 2
|
|
|
|
btn = QPushButton(f"Profile {profile_id}\n{profile.name}")
|
|
btn.setCheckable(True)
|
|
btn.setFixedSize(180, 60)
|
|
btn.setParent(self.delete_scroll_widget)
|
|
btn.setGeometry(col * 220 + 50, row * 100, 180, 60)
|
|
btn.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border: none;
|
|
color: white;
|
|
border-radius: 5px;
|
|
text-align: center;
|
|
{self.font_style}
|
|
}}
|
|
QPushButton:checked {{
|
|
background: rgba(255, 255, 255, 0.3);
|
|
border: 2px solid #007AFF;
|
|
}}
|
|
QPushButton:hover:!checked {{
|
|
background: rgba(255, 255, 255, 0.2);
|
|
}}
|
|
""")
|
|
self.profile_buttons.addButton(btn, profile_id)
|
|
|
|
if profiles:
|
|
rows = (len(profiles) + 1) // 2
|
|
height = rows * 100
|
|
self.delete_scroll_widget.setFixedSize(510, height)
|
|
|
|
def create_debug_page(self):
|
|
page = QWidget()
|
|
layout = QVBoxLayout(page)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
layout.setSpacing(15)
|
|
|
|
title = QLabel("DEBUG HELP")
|
|
title.setStyleSheet(
|
|
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
layout.addWidget(title)
|
|
|
|
scroll_area = QScrollArea()
|
|
scroll_area.setWidgetResizable(True)
|
|
scroll_area.setStyleSheet(f"""
|
|
QScrollArea {{
|
|
border: none;
|
|
background: transparent;
|
|
}}
|
|
QScrollArea > QWidget > QWidget {{
|
|
background: transparent;
|
|
}}
|
|
""")
|
|
|
|
scroll_content = QWidget()
|
|
scroll_layout = QVBoxLayout(scroll_content)
|
|
scroll_layout.setSpacing(15)
|
|
|
|
profile_selection_group = QGroupBox("Profile Selection")
|
|
profile_selection_group.setStyleSheet(f"""
|
|
QGroupBox {{
|
|
color: white;
|
|
font-weight: bold;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
font-size: 10px;
|
|
margin-top: 15px;
|
|
{self.font_style}
|
|
}}
|
|
QGroupBox::title {{
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 5px;
|
|
}}
|
|
""")
|
|
profile_selection_layout = QVBoxLayout(profile_selection_group)
|
|
|
|
profile_label = QLabel("Select System-wide Profile:")
|
|
profile_label.setStyleSheet(
|
|
f"color: white; font-size: 12px; {self.font_style}")
|
|
profile_selection_layout.addWidget(profile_label)
|
|
|
|
self.debug_profile_selector = QComboBox()
|
|
self.debug_profile_selector.setStyleSheet(combobox_style(self.font_style))
|
|
self.debug_profile_selector.currentTextChanged.connect(
|
|
self.on_debug_profile_selected)
|
|
profile_selection_layout.addWidget(self.debug_profile_selector)
|
|
|
|
scroll_layout.addWidget(profile_selection_group)
|
|
|
|
ping_group = QGroupBox("Ping Test")
|
|
ping_group.setStyleSheet(f"""
|
|
QGroupBox {{
|
|
color: white;
|
|
font-weight: bold;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
font-size: 10px;
|
|
margin-top: 15px;
|
|
{self.font_style}
|
|
}}
|
|
QGroupBox::title {{
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 5px;
|
|
}}
|
|
""")
|
|
ping_layout = QVBoxLayout(ping_group)
|
|
|
|
self.ping_instruction_label = QLabel("")
|
|
self.ping_instruction_label.setStyleSheet(
|
|
f"color: white; font-size: 12px; {self.font_style}")
|
|
self.ping_instruction_label.setWordWrap(True)
|
|
self.ping_instruction_label.hide()
|
|
ping_layout.addWidget(self.ping_instruction_label)
|
|
|
|
ping_buttons_layout = QHBoxLayout()
|
|
|
|
self.copy_ping_button = QPushButton("Copy Ping Command")
|
|
self.copy_ping_button.setFixedSize(140, 40)
|
|
self.copy_ping_button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
font-size: 12px;
|
|
background: #007AFF;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
font-weight: bold;
|
|
{self.font_style}
|
|
}}
|
|
QPushButton:hover {{
|
|
background: #0056CC;
|
|
}}
|
|
QPushButton:disabled {{
|
|
background: #666666;
|
|
}}
|
|
""")
|
|
self.copy_ping_button.clicked.connect(self.copy_ping_command)
|
|
self.copy_ping_button.setEnabled(False)
|
|
ping_buttons_layout.addWidget(self.copy_ping_button)
|
|
|
|
self.test_ping_button = QPushButton("Test Ping")
|
|
self.test_ping_button.setFixedSize(120, 40)
|
|
self.test_ping_button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
font-size: 12px;
|
|
background: #4CAF50;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
font-weight: bold;
|
|
{self.font_style}
|
|
}}
|
|
QPushButton:hover {{
|
|
background: #45a049;
|
|
}}
|
|
QPushButton:disabled {{
|
|
background: #666666;
|
|
}}
|
|
""")
|
|
self.test_ping_button.clicked.connect(self.test_ping_connection)
|
|
self.test_ping_button.setEnabled(False)
|
|
ping_buttons_layout.addWidget(self.test_ping_button)
|
|
|
|
ping_layout.addLayout(ping_buttons_layout)
|
|
|
|
self.ping_result_label = QLabel("")
|
|
self.ping_result_label.setStyleSheet(
|
|
f"color: white; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
self.ping_result_label.setWordWrap(True)
|
|
self.ping_result_label.hide()
|
|
ping_layout.addWidget(self.ping_result_label)
|
|
|
|
scroll_layout.addWidget(ping_group)
|
|
|
|
command_group = QGroupBox("CLI Commands")
|
|
command_group.setStyleSheet(f"""
|
|
QGroupBox {{
|
|
color: white;
|
|
font-weight: bold;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
font-size: 10px;
|
|
margin-top: 15px;
|
|
{self.font_style}
|
|
}}
|
|
QGroupBox::title {{
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 5px;
|
|
}}
|
|
""")
|
|
command_layout = QVBoxLayout(command_group)
|
|
|
|
self.cli_command = QLineEdit()
|
|
self.cli_command.setReadOnly(True)
|
|
self.cli_command.setText("Select a profile above to see command")
|
|
self.cli_command.setStyleSheet(f"""
|
|
QLineEdit {{
|
|
color: white;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
}}
|
|
""")
|
|
command_layout.addWidget(self.cli_command)
|
|
|
|
cli_buttons_layout = QHBoxLayout()
|
|
|
|
cli_copy_button = QPushButton("Copy CLI Command")
|
|
cli_copy_button.setFixedSize(140, 40)
|
|
cli_copy_button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
font-size: 12px;
|
|
background: #007AFF;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
font-weight: bold;
|
|
{self.font_style}
|
|
}}
|
|
QPushButton:hover {{
|
|
background: #0056CC;
|
|
}}
|
|
QPushButton:disabled {{
|
|
background: #666666;
|
|
}}
|
|
""")
|
|
cli_copy_button.clicked.connect(self.copy_cli_command)
|
|
cli_copy_button.setEnabled(False)
|
|
cli_buttons_layout.addWidget(cli_copy_button)
|
|
|
|
command_layout.addLayout(cli_buttons_layout)
|
|
|
|
find_buttons_layout = QHBoxLayout()
|
|
|
|
command_layout.addLayout(find_buttons_layout)
|
|
|
|
scroll_layout.addWidget(command_group)
|
|
|
|
self.cli_copy_button = cli_copy_button
|
|
|
|
wg_quick_group = QGroupBox("Direct Systemwide Wireguard")
|
|
wg_quick_group.setStyleSheet(f"""
|
|
QGroupBox {{
|
|
color: white;
|
|
font-weight: bold;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
font-size: 10px;
|
|
margin-top: 15px;
|
|
{self.font_style}
|
|
}}
|
|
QGroupBox::title {{
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 5px;
|
|
}}
|
|
""")
|
|
wg_quick_layout = QVBoxLayout(wg_quick_group)
|
|
|
|
wg_quick_subtitle = QLabel("This uses your Linux system's Wireguard")
|
|
wg_quick_subtitle.setStyleSheet(
|
|
f"font-size: 10px; color: #CCCCCC; {self.font_style}")
|
|
wg_quick_subtitle.setWordWrap(True)
|
|
wg_quick_layout.addWidget(wg_quick_subtitle)
|
|
|
|
self.wg_quick_up_command = QLineEdit()
|
|
self.wg_quick_up_command.setReadOnly(True)
|
|
self.wg_quick_up_command.setText(
|
|
"Select a profile above to see commands")
|
|
self.wg_quick_up_command.setStyleSheet(f"""
|
|
QLineEdit {{
|
|
color: black;
|
|
background: white;
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
}}
|
|
""")
|
|
wg_quick_layout.addWidget(self.wg_quick_up_command)
|
|
|
|
copy_wg_up_button = QPushButton("Copy UP command")
|
|
copy_wg_up_button.setFixedSize(140, 40)
|
|
copy_wg_up_button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
font-size: 10px;
|
|
background: #4CAF50;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
font-weight: bold;
|
|
{self.font_style}
|
|
}}
|
|
QPushButton:hover {{
|
|
background: #45a049;
|
|
}}
|
|
""")
|
|
copy_wg_up_button.clicked.connect(self.copy_wg_quick_up_command)
|
|
copy_wg_up_button.setEnabled(False)
|
|
wg_quick_layout.addWidget(copy_wg_up_button)
|
|
|
|
self.wg_quick_down_command = QLineEdit()
|
|
self.wg_quick_down_command.setReadOnly(True)
|
|
self.wg_quick_down_command.setText(
|
|
"Select a profile above to see commands")
|
|
self.wg_quick_down_command.setStyleSheet(f"""
|
|
QLineEdit {{
|
|
color: black;
|
|
background: white;
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
}}
|
|
""")
|
|
wg_quick_layout.addWidget(self.wg_quick_down_command)
|
|
|
|
copy_wg_down_button = QPushButton("Copy DOWN command")
|
|
copy_wg_down_button.setFixedSize(140, 40)
|
|
copy_wg_down_button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
font-size: 10px;
|
|
background: #F44336;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
font-weight: bold;
|
|
{self.font_style}
|
|
}}
|
|
QPushButton:hover {{
|
|
background: #d32f2f;
|
|
}}
|
|
""")
|
|
copy_wg_down_button.clicked.connect(self.copy_wg_quick_down_command)
|
|
copy_wg_down_button.setEnabled(False)
|
|
wg_quick_layout.addWidget(copy_wg_down_button)
|
|
|
|
self.wg_quick_up_button = copy_wg_up_button
|
|
self.wg_quick_down_button = copy_wg_down_button
|
|
|
|
self.wg_quick_up_command_widget = self.wg_quick_up_command
|
|
self.wg_quick_down_command_widget = self.wg_quick_down_command
|
|
|
|
scroll_layout.addWidget(wg_quick_group)
|
|
|
|
self.update_debug_profile_list()
|
|
|
|
scroll_area.setWidget(scroll_content)
|
|
layout.addWidget(scroll_area)
|
|
|
|
return page
|
|
|
|
def copy_cli_command(self):
|
|
clipboard = QApplication.clipboard()
|
|
clipboard.setText(self.cli_command.text())
|
|
self.update_status.update_status("CLI command copied to clipboard")
|
|
|
|
def execute_cli_command(self):
|
|
try:
|
|
command = self.cli_command.text().split()
|
|
subprocess.Popen(command)
|
|
self.update_status.update_status("CLI command executed")
|
|
except Exception as e:
|
|
self.update_status.update_status(
|
|
f"Error executing command: {str(e)}")
|
|
|
|
def copy_find_command(self, command_text):
|
|
clipboard = QApplication.clipboard()
|
|
clipboard.setText(command_text)
|
|
self.update_status.update_status("Find command copied to clipboard")
|
|
|
|
def execute_find_command(self, command_text):
|
|
try:
|
|
command = command_text.split()
|
|
subprocess.Popen(command)
|
|
self.update_status.update_status("Find command executed")
|
|
except Exception as e:
|
|
self.update_status.update_status(
|
|
f"Error executing find command: {str(e)}")
|
|
|
|
def update_debug_profile_list(self):
|
|
self.debug_profile_selector.clear()
|
|
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
|
system_profiles = {pid: profile for pid, profile in profiles.items(
|
|
) if isinstance(profile, SystemProfile)}
|
|
|
|
if system_profiles:
|
|
for profile_id, profile in system_profiles.items():
|
|
self.debug_profile_selector.addItem(
|
|
f"Profile {profile_id}: {profile.name}", profile_id)
|
|
else:
|
|
self.debug_profile_selector.addItem(
|
|
"No system-wide profiles found", None)
|
|
|
|
def on_debug_profile_selected(self, text):
|
|
profile_id = self.debug_profile_selector.currentData()
|
|
if profile_id is None:
|
|
self.ping_instruction_label.hide()
|
|
self.copy_ping_button.setEnabled(False)
|
|
self.test_ping_button.setEnabled(False)
|
|
self.ping_result_label.hide()
|
|
self.cli_command.setText("Select a profile above to see command")
|
|
self.cli_copy_button.setEnabled(False)
|
|
if hasattr(self, 'wg_quick_up_command_widget'):
|
|
self.wg_quick_up_command_widget.setText(
|
|
"Select a profile above to see commands")
|
|
self.wg_quick_down_command_widget.setText(
|
|
"Select a profile above to see commands")
|
|
self.wg_quick_up_button.setEnabled(False)
|
|
self.wg_quick_down_button.setEnabled(False)
|
|
return
|
|
|
|
profile = ProfileController.get(profile_id)
|
|
|
|
if profile and isinstance(profile, SystemProfile):
|
|
app_path = os.environ.get('APPIMAGE') or "/path/to/hydra-veil"
|
|
self.cli_command.setText(
|
|
f"'{app_path}' --cli profile enable -i {profile_id}")
|
|
self.cli_copy_button.setEnabled(True)
|
|
ip_address = settings_data.extract_endpoint_ip(profile)
|
|
if ip_address:
|
|
self.ping_instruction_label.setText(
|
|
f"Step 1, Can you Ping it? Copy-paste the command below into your terminal. This VPN Node's IP address is {ip_address}")
|
|
self.ping_instruction_label.show()
|
|
self.copy_ping_button.setEnabled(True)
|
|
self.test_ping_button.setEnabled(True)
|
|
self.ping_result_label.hide()
|
|
|
|
if hasattr(self, 'wg_quick_up_command_widget'):
|
|
self.wg_quick_up_command_widget.setText(
|
|
f"sudo wg-quick up '/etc/hydra-veil/profiles/{profile_id}/wg.conf'")
|
|
self.wg_quick_down_command_widget.setText(
|
|
f"sudo wg-quick down '/etc/hydra-veil/profiles/{profile_id}/wg.conf'")
|
|
self.wg_quick_up_button.setEnabled(True)
|
|
self.wg_quick_down_button.setEnabled(True)
|
|
else:
|
|
self.ping_instruction_label.setText(
|
|
"Could not extract IP address from WireGuard configuration")
|
|
self.ping_instruction_label.show()
|
|
self.copy_ping_button.setEnabled(False)
|
|
self.test_ping_button.setEnabled(False)
|
|
self.ping_result_label.hide()
|
|
|
|
if hasattr(self, 'wg_quick_up_command_widget'):
|
|
self.wg_quick_up_command_widget.setText(
|
|
"Could not load profile configuration")
|
|
self.wg_quick_down_command_widget.setText(
|
|
"Could not load profile configuration")
|
|
self.wg_quick_up_button.setEnabled(False)
|
|
self.wg_quick_down_button.setEnabled(False)
|
|
|
|
def copy_ping_command(self):
|
|
profile_id = self.debug_profile_selector.currentData()
|
|
if profile_id is None:
|
|
return
|
|
|
|
profile = ProfileController.get(profile_id)
|
|
if profile and isinstance(profile, SystemProfile):
|
|
ip_address = settings_data.extract_endpoint_ip(profile)
|
|
if ip_address:
|
|
ping_command = f"ping {ip_address}"
|
|
clipboard = QApplication.clipboard()
|
|
clipboard.setText(ping_command)
|
|
self.update_status.update_status(
|
|
"Ping command copied to clipboard")
|
|
|
|
def test_ping_connection(self):
|
|
profile_id = self.debug_profile_selector.currentData()
|
|
if profile_id is None:
|
|
return
|
|
|
|
profile = ProfileController.get(profile_id)
|
|
if profile and isinstance(profile, SystemProfile):
|
|
ip_address = settings_data.extract_endpoint_ip(profile)
|
|
if ip_address:
|
|
self.test_ping_button.setEnabled(False)
|
|
self.ping_result_label.setText("Testing ping...")
|
|
self.ping_result_label.setStyleSheet(
|
|
f"color: #FFA500; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
self.ping_result_label.show()
|
|
|
|
def ping_test():
|
|
try:
|
|
if sys.platform == "win32":
|
|
result = subprocess.run(['ping', '-n', '4', ip_address],
|
|
capture_output=True, text=True, timeout=10)
|
|
else:
|
|
result = subprocess.run(['ping', '-c', '4', ip_address],
|
|
capture_output=True, text=True, timeout=10)
|
|
if result.returncode == 0:
|
|
self.ping_result_label.setText(
|
|
"✅ Ping successful - Connection is working!")
|
|
self.ping_result_label.setStyleSheet(
|
|
f"color: #4CAF50; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
else:
|
|
self.ping_result_label.setText(
|
|
"❌ Ping failed - Connection issue detected")
|
|
self.ping_result_label.setStyleSheet(
|
|
f"color: #F44336; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
except subprocess.TimeoutExpired:
|
|
self.ping_result_label.setText(
|
|
"❌ Ping timeout - Connection issue detected")
|
|
self.ping_result_label.setStyleSheet(
|
|
f"color: #F44336; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
except Exception as e:
|
|
self.ping_result_label.setText(
|
|
f"❌ Ping error: {str(e)}")
|
|
self.ping_result_label.setStyleSheet(
|
|
f"color: #F44336; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
finally:
|
|
self.test_ping_button.setEnabled(True)
|
|
|
|
threading.Thread(target=ping_test, daemon=True).start()
|
|
|
|
def copy_wg_quick_up_command(self):
|
|
clipboard = QApplication.clipboard()
|
|
clipboard.setText(self.wg_quick_up_command_widget.text())
|
|
self.update_status.update_status(
|
|
"wg-quick UP command copied to clipboard")
|
|
|
|
def copy_wg_quick_down_command(self):
|
|
clipboard = QApplication.clipboard()
|
|
clipboard.setText(self.wg_quick_down_command_widget.text())
|
|
self.update_status.update_status(
|
|
"wg-quick DOWN command copied to clipboard")
|
|
|
|
def on_profile_selected(self, button):
|
|
self.delete_button.setEnabled(True)
|
|
self.selected_profile_id = self.profile_buttons.id(button)
|
|
|
|
def delete_selected_profile(self):
|
|
if hasattr(self, 'selected_profile_id'):
|
|
self.popup = ConfirmationPopup(
|
|
self,
|
|
message=f"Are you sure you want to delete Profile {self.selected_profile_id}?",
|
|
action_button_text="Delete",
|
|
cancel_button_text="Cancel"
|
|
)
|
|
self.popup.setWindowModality(Qt.WindowModality.ApplicationModal)
|
|
self.popup.finished.connect(self.handle_delete_confirmation)
|
|
self.popup.show()
|
|
|
|
def handle_delete_confirmation(self, confirmed):
|
|
if confirmed:
|
|
self.update_status.update_status(f'Deleting profile...')
|
|
self.worker_thread = WorkerThread('DESTROY_PROFILE', profile_data={
|
|
'id': self.selected_profile_id})
|
|
self.worker_thread.text_output.connect(self.delete_status_update)
|
|
self.worker_thread.start()
|
|
|
|
def delete_status_update(self, message):
|
|
self.update_status.update_status(message)
|
|
if not message.startswith('Could not delete'):
|
|
menu_page = self.custom_window.navigator.get_cached("menu")
|
|
if menu_page is not None and hasattr(menu_page, 'clear_profile_details'):
|
|
menu_page.clear_profile_details()
|
|
if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'):
|
|
menu_page.refresh_menu_buttons()
|
|
self.content_layout.removeWidget(self.delete_page)
|
|
self.delete_page = self.create_delete_page()
|
|
self.content_layout.addWidget(self.delete_page)
|
|
self.content_layout.setCurrentWidget(self.delete_page)
|
|
|
|
def get_checkbox_style(self) -> str:
|
|
return checkbox_style(self.font_style)
|
|
|
|
def reverse(self):
|
|
self.custom_window.navigator.navigate("menu")
|
|
|
|
def create_subscription_page(self):
|
|
page = QWidget()
|
|
layout = QVBoxLayout(page)
|
|
layout.setSpacing(20)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
|
|
title = QLabel("Subscription Info")
|
|
title.setStyleSheet(
|
|
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
layout.addWidget(title)
|
|
|
|
profile_group = QGroupBox("Profile Selection")
|
|
profile_group.setStyleSheet(f"""
|
|
QGroupBox {{
|
|
color: white;
|
|
font-weight: bold;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-top: 15px;
|
|
{self.font_style}
|
|
}}
|
|
QGroupBox::title {{
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 5px;
|
|
}}
|
|
""")
|
|
profile_layout = QVBoxLayout(profile_group)
|
|
|
|
self.profile_selector = QComboBox()
|
|
self.profile_selector.setStyleSheet(combobox_style(self.font_style))
|
|
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
|
if profiles:
|
|
for profile_id, profile in profiles.items():
|
|
self.profile_selector.addItem(
|
|
f"Profile {profile_id}: {profile.name}", profile_id)
|
|
profile_layout.addWidget(self.profile_selector)
|
|
layout.addWidget(profile_group)
|
|
|
|
subscription_group = QGroupBox("Subscription Details")
|
|
subscription_group.setStyleSheet(f"""
|
|
QGroupBox {{
|
|
color: white;
|
|
font-weight: bold;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-top: 15px;
|
|
{self.font_style}
|
|
}}
|
|
QGroupBox::title {{
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 5px;
|
|
}}
|
|
""")
|
|
subscription_layout = QGridLayout(subscription_group)
|
|
subscription_layout.setSpacing(10)
|
|
|
|
self.subscription_info = {}
|
|
info_items = [
|
|
("Billing Code", "billing_code"),
|
|
("Expires At", "expires_at")
|
|
]
|
|
|
|
for i, (label, key) in enumerate(info_items):
|
|
if key == "billing_code":
|
|
billing_container = QWidget()
|
|
billing_layout = QVBoxLayout(billing_container)
|
|
billing_layout.setContentsMargins(0, 0, 0, 0)
|
|
billing_layout.setSpacing(5)
|
|
|
|
stat_widget = self.create_stat_widget(label, "N/A")
|
|
billing_layout.addWidget(stat_widget)
|
|
|
|
copy_button = QPushButton("Copy")
|
|
copy_button.setFixedSize(60, 30)
|
|
copy_button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background: #007AFF;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-weight: bold;
|
|
{self.font_style}
|
|
}}
|
|
QPushButton:hover {{
|
|
background: #0056CC;
|
|
}}
|
|
""")
|
|
copy_button.clicked.connect(lambda: self.copy_billing_code())
|
|
billing_layout.addWidget(copy_button)
|
|
|
|
row, col = divmod(i, 2)
|
|
subscription_layout.addWidget(billing_container, row, col)
|
|
|
|
old_label = stat_widget.findChild(QLabel, "value_label")
|
|
if old_label:
|
|
old_label.setParent(None)
|
|
old_label.deleteLater()
|
|
|
|
value_label = ClickableValueLabel("N/A", stat_widget)
|
|
value_label.setObjectName("value_label")
|
|
value_label.setStyleSheet(
|
|
f"color: #00ffff; font-size: 12px; font-family: 'Retro Gaming', sans-serif;")
|
|
value_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
stat_widget.layout().insertWidget(0, value_label)
|
|
|
|
self.subscription_info[key] = value_label
|
|
else:
|
|
stat_widget = self.create_stat_widget(label, "N/A")
|
|
row, col = divmod(i, 2)
|
|
subscription_layout.addWidget(stat_widget, row, col)
|
|
|
|
old_label = stat_widget.findChild(QLabel, "value_label")
|
|
if old_label:
|
|
old_label.setParent(None)
|
|
old_label.deleteLater()
|
|
|
|
value_label = ClickableValueLabel("N/A", stat_widget)
|
|
value_label.setObjectName("value_label")
|
|
value_label.setStyleSheet(
|
|
f"color: #00ffff; font-size: 12px; font-family: 'Retro Gaming', sans-serif;")
|
|
value_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
stat_widget.layout().insertWidget(0, value_label)
|
|
|
|
self.subscription_info[key] = value_label
|
|
|
|
layout.addWidget(subscription_group)
|
|
|
|
clipboard_hint = QLabel(
|
|
"💡 Click on the billing code or use the Copy button to copy it to clipboard")
|
|
clipboard_hint.setStyleSheet(
|
|
f"color: #666666; font-size: 11px; font-style: italic; {self.font_style}")
|
|
clipboard_hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
layout.addWidget(clipboard_hint)
|
|
|
|
layout.addStretch()
|
|
|
|
self.profile_selector.currentIndexChanged.connect(
|
|
self.update_subscription_info)
|
|
if self.profile_selector.count() > 0:
|
|
self.update_subscription_info(0)
|
|
|
|
return page
|
|
|
|
def create_verification_page(self):
|
|
page = QWidget()
|
|
layout = QVBoxLayout(page)
|
|
layout.setSpacing(20)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
|
|
title = QLabel("Verification Information")
|
|
title.setStyleSheet(
|
|
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
layout.addWidget(title)
|
|
|
|
profile_label = QLabel("Profile Selection:")
|
|
profile_label.setStyleSheet(
|
|
f"color: white; font-size: 14px; {self.font_style}")
|
|
layout.addWidget(profile_label)
|
|
|
|
self.verification_profile_selector = QComboBox()
|
|
self.verification_profile_selector.setStyleSheet(
|
|
combobox_style(self.font_style))
|
|
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
|
if profiles:
|
|
for profile_id, profile in profiles.items():
|
|
self.verification_profile_selector.addItem(
|
|
f"Profile {profile_id}: {profile.name}", profile_id)
|
|
layout.addWidget(self.verification_profile_selector)
|
|
|
|
details_label = QLabel("Verification Details:")
|
|
details_label.setStyleSheet(
|
|
f"color: white; font-size: 14px; margin-top: 20px; {self.font_style}")
|
|
layout.addWidget(details_label)
|
|
|
|
verification_layout = QVBoxLayout()
|
|
verification_layout.setSpacing(15)
|
|
self.verification_info = {}
|
|
self.verification_checkmarks = {}
|
|
self.verification_full_values = {}
|
|
info_items = [
|
|
("Operator Name", "operator_name"),
|
|
("Nostr Key", "nostr_public_key"),
|
|
("HydraVeil Key", "hydraveil_public_key"),
|
|
("Nostr Verification", "nostr_attestation_event_reference"),
|
|
]
|
|
|
|
for label, key in info_items:
|
|
container = QWidget()
|
|
container_layout = QHBoxLayout(container)
|
|
container_layout.setContentsMargins(0, 0, 0, 0)
|
|
container_layout.setSpacing(10)
|
|
|
|
label_widget = QLabel(label + ":")
|
|
label_widget.setStyleSheet(
|
|
f"color: white; font-size: 12px; {self.font_style}")
|
|
label_widget.setMinimumWidth(120)
|
|
container_layout.addWidget(label_widget)
|
|
|
|
value_widget = QLineEdit()
|
|
value_widget.setReadOnly(True)
|
|
value_widget.setText("N/A")
|
|
value_widget.setStyleSheet(f"""
|
|
QLineEdit {{
|
|
color: white;
|
|
background: transparent;
|
|
border: none;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
|
padding: 5px 0px;
|
|
font-size: 12px;
|
|
{self.font_style}
|
|
}}
|
|
""")
|
|
container_layout.addWidget(value_widget, 1)
|
|
|
|
checkmark_widget = QLabel("✓")
|
|
checkmark_widget.setStyleSheet(
|
|
f"color: #4CAF50; font-size: 20px; font-weight: bold; {self.font_style}")
|
|
checkmark_widget.setFixedSize(20, 20)
|
|
checkmark_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
checkmark_widget.hide()
|
|
container_layout.addWidget(checkmark_widget)
|
|
|
|
copy_button = QPushButton()
|
|
copy_button.setIcon(
|
|
QIcon(os.path.join(self.btn_path, "paste_button.png")))
|
|
copy_button.setIconSize(QSize(24, 24))
|
|
copy_button.setFixedSize(30, 30)
|
|
copy_button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background: transparent;
|
|
border: none;
|
|
}}
|
|
QPushButton:hover {{
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 4px;
|
|
}}
|
|
""")
|
|
copy_button.clicked.connect(
|
|
lambda checked=False, k=key: self.copy_verification_value(k))
|
|
container_layout.addWidget(copy_button)
|
|
|
|
verification_layout.addWidget(container)
|
|
self.verification_info[key] = value_widget
|
|
self.verification_checkmarks[key] = checkmark_widget
|
|
|
|
layout.addLayout(verification_layout)
|
|
layout.addStretch()
|
|
|
|
endpoint_verification_label = QLabel("Endpoint Verification:", page)
|
|
endpoint_verification_label.setGeometry(20, 420, 150, 20)
|
|
endpoint_verification_label.setStyleSheet(
|
|
f"color: white; font-size: 15px; {self.font_style}")
|
|
endpoint_verification_label.show()
|
|
|
|
self.endpoint_verification_checkbox = QCheckBox(page)
|
|
self.endpoint_verification_checkbox.setGeometry(180, 415, 30, 30)
|
|
self.endpoint_verification_checkbox.setChecked(
|
|
self._prepared_value(
|
|
"endpoint_verification_enabled",
|
|
ConfigurationController.get_endpoint_verification_enabled))
|
|
self.endpoint_verification_checkbox.setStyleSheet(
|
|
self.get_checkbox_style())
|
|
self.endpoint_verification_checkbox.show()
|
|
|
|
save_button = QPushButton(page)
|
|
save_button.setGeometry(350, 410, 60, 31)
|
|
save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png")))
|
|
save_button.setIconSize(QSize(60, 31))
|
|
save_button.clicked.connect(self.save_verification_settings)
|
|
save_button.show()
|
|
|
|
self.verification_profile_selector.currentIndexChanged.connect(
|
|
self.update_verification_info)
|
|
self.verification_profile_selector.activated.connect(
|
|
self.update_verification_info)
|
|
if self.verification_profile_selector.count() > 0:
|
|
self.update_verification_info(0)
|
|
|
|
return page
|
|
|
|
def update_verification_info(self, index):
|
|
if index < 0:
|
|
return
|
|
|
|
profile_id = self.verification_profile_selector.itemData(index)
|
|
profile = None
|
|
if profile_id is not None:
|
|
profile = ProfileController.get(profile_id)
|
|
|
|
view = settings_data.build_verification_view(profile)
|
|
truncated_keys = (
|
|
"nostr_public_key",
|
|
"hydraveil_public_key",
|
|
"nostr_attestation_event_reference",
|
|
)
|
|
|
|
for key, widget in self.verification_info.items():
|
|
full_value = view.get(key, "N/A")
|
|
self.verification_full_values[key] = full_value
|
|
if key in truncated_keys and full_value != "N/A":
|
|
widget.setText(settings_data.truncate_key(full_value))
|
|
else:
|
|
widget.setText(full_value)
|
|
if key in self.verification_checkmarks:
|
|
if full_value and full_value != "N/A":
|
|
self.verification_checkmarks[key].show()
|
|
else:
|
|
self.verification_checkmarks[key].hide()
|
|
|
|
def save_verification_settings(self):
|
|
try:
|
|
ConfigurationController.set_endpoint_verification_enabled(
|
|
self.endpoint_verification_checkbox.isChecked())
|
|
self.update_status.update_status(
|
|
"Verification settings saved successfully")
|
|
except Exception as e:
|
|
logging.error(f"Error saving verification settings: {str(e)}")
|
|
self.update_status.update_status(
|
|
"Error saving verification settings")
|
|
|
|
def copy_verification_value(self, key):
|
|
full_value = self.verification_full_values.get(key, "")
|
|
if full_value and full_value != "N/A":
|
|
clipboard = QApplication.clipboard()
|
|
clipboard.setText(full_value)
|
|
verification_display_names = {
|
|
"operator_name": "Operator Name",
|
|
"nostr_public_key": "Nostr Key",
|
|
"hydraveil_public_key": "HydraVeil Key",
|
|
"nostr_attestation_event_reference": "Nostr Verification"
|
|
}
|
|
display_name = verification_display_names.get(
|
|
key, "Verification value")
|
|
self.update_status.update_status(
|
|
f"{display_name} copied to clipboard")
|
|
|
|
def update_subscription_info(self, index):
|
|
if index < 0:
|
|
return
|
|
|
|
profile_id = self.profile_selector.itemData(index)
|
|
if profile_id is None:
|
|
return
|
|
|
|
profile = ProfileController.get(profile_id)
|
|
|
|
if profile and hasattr(profile, 'subscription') and profile.subscription:
|
|
try:
|
|
view = settings_data.build_subscription_view(profile)
|
|
self.subscription_info["billing_code"].setText(view["billing_code"])
|
|
self.subscription_info["expires_at"].setText(view["expires_at"])
|
|
except Exception as e:
|
|
print(f"Error updating subscription info: {e}")
|
|
else:
|
|
for label in self.subscription_info.values():
|
|
label.setText("N/A")
|
|
|
|
def copy_billing_code(self):
|
|
if "billing_code" in self.subscription_info:
|
|
billing_code = self.subscription_info["billing_code"].text()
|
|
if billing_code and billing_code != "N/A":
|
|
clipboard = QApplication.clipboard()
|
|
clipboard.setText(billing_code)
|
|
self.update_status.update_status(
|
|
"Billing code copied to clipboard")
|
|
else:
|
|
self.update_status.update_status(
|
|
"No billing code available to copy")
|
|
|
|
def showEvent(self, event):
|
|
super().showEvent(event)
|
|
self._settings_refresh_seq += 1
|
|
seq = self._settings_refresh_seq
|
|
module_path, class_name = PAGE_REGISTRY["settings"]
|
|
worker = PageDataWorker(
|
|
"settings", module_path, class_name, self.custom_window)
|
|
worker.data_ready.connect(
|
|
lambda name, payload, s=seq: self._on_settings_data_ready(payload, s))
|
|
worker.failed.connect(
|
|
lambda name, error: self._on_settings_data_failed(error))
|
|
worker.finished.connect(
|
|
lambda w=worker: self._cleanup_settings_worker(w))
|
|
self._settings_workers.add(worker)
|
|
worker.start()
|
|
|
|
def _on_settings_data_failed(self, error):
|
|
core_logger.warning(f"Settings data refresh failed: {error}")
|
|
|
|
def _cleanup_settings_worker(self, worker):
|
|
self._settings_workers.discard(worker)
|
|
worker.deleteLater()
|
|
|
|
def _on_settings_data_ready(self, payload, seq):
|
|
if seq != self._settings_refresh_seq:
|
|
return
|
|
self._prepared = payload
|
|
|
|
current_index = self.content_layout.currentIndex()
|
|
|
|
self.content_layout.removeWidget(self.account_page)
|
|
self.account_page = self.create_account_page()
|
|
self.content_layout.addWidget(self.account_page)
|
|
|
|
self.content_layout.removeWidget(self.subscription_page)
|
|
self.subscription_page = self.create_subscription_page()
|
|
self.content_layout.addWidget(self.subscription_page)
|
|
|
|
self.content_layout.removeWidget(self.tickets_page)
|
|
self.tickets_page = self.create_tickets_page()
|
|
self.content_layout.addWidget(self.tickets_page)
|
|
|
|
self.content_layout.removeWidget(self.registrations_page)
|
|
self.registrations_page = self.create_registrations_page()
|
|
self.content_layout.addWidget(self.registrations_page)
|
|
|
|
self.content_layout.removeWidget(self.verification_page)
|
|
self.verification_page = self.create_verification_page()
|
|
self.content_layout.addWidget(self.verification_page)
|
|
|
|
self.content_layout.removeWidget(self.systemwide_page)
|
|
self.systemwide_page = self.create_systemwide_page()
|
|
self.content_layout.addWidget(self.systemwide_page)
|
|
|
|
self.content_layout.removeWidget(self.bwrap_page)
|
|
self.bwrap_page = self.create_bwrap_page()
|
|
self.content_layout.addWidget(self.bwrap_page)
|
|
|
|
self.content_layout.removeWidget(self.delete_page)
|
|
self.delete_page = self.create_delete_page()
|
|
self.content_layout.addWidget(self.delete_page)
|
|
|
|
self.content_layout.removeWidget(self.logs_page)
|
|
self.logs_page = self.create_logs_page()
|
|
self.content_layout.addWidget(self.logs_page)
|
|
|
|
self.content_layout.removeWidget(self.debug_page)
|
|
self.debug_page = self.create_debug_page()
|
|
self.content_layout.addWidget(self.debug_page)
|
|
|
|
self.content_layout.setCurrentIndex(current_index)
|
|
|
|
if self.content_layout.currentWidget() == self.subscription_page:
|
|
if self.profile_selector.count() > 0:
|
|
self.update_subscription_info(0)
|
|
|
|
def create_account_page(self):
|
|
page = QWidget()
|
|
layout = QVBoxLayout(page)
|
|
layout.setSpacing(20)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
|
|
title = QLabel("Account Overview")
|
|
title.setStyleSheet(
|
|
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
layout.addWidget(title)
|
|
|
|
scroll_area = QScrollArea()
|
|
scroll_area.setWidgetResizable(True)
|
|
scroll_area.setStyleSheet(f"""
|
|
QScrollArea {{
|
|
border: none;
|
|
background: transparent;
|
|
}}
|
|
QScrollArea > QWidget > QWidget {{
|
|
background: transparent;
|
|
}}
|
|
""")
|
|
|
|
scroll_content = QWidget()
|
|
scroll_layout = QVBoxLayout(scroll_content)
|
|
scroll_layout.setSpacing(15)
|
|
|
|
info_sections = [
|
|
("Profile Statistics", self.create_profile_stats()),
|
|
("System Resources", self.create_system_stats())
|
|
]
|
|
|
|
for section_title, content_widget in info_sections:
|
|
group = QGroupBox(section_title)
|
|
group.setStyleSheet(f"""
|
|
QGroupBox {{
|
|
color: white;
|
|
font-weight: bold;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-top: 15px;
|
|
{self.font_style}
|
|
}}
|
|
QGroupBox::title {{
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 5px;
|
|
}}
|
|
""")
|
|
group_layout = QVBoxLayout(group)
|
|
group_layout.addWidget(content_widget)
|
|
scroll_layout.addWidget(group)
|
|
|
|
scroll_area.setWidget(scroll_content)
|
|
layout.addWidget(scroll_area)
|
|
return page
|
|
|
|
def create_profile_stats(self):
|
|
widget = QWidget()
|
|
layout = QGridLayout(widget)
|
|
layout.setSpacing(10)
|
|
|
|
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
|
total_profiles = len(profiles)
|
|
session_profiles = sum(1 for p in profiles.values()
|
|
if isinstance(p, SessionProfile))
|
|
system_profiles = sum(1 for p in profiles.values()
|
|
if isinstance(p, SystemProfile))
|
|
active_profiles = sum(
|
|
1 for p in profiles.values()
|
|
if p.subscription and p.subscription.expires_at and p.subscription.expires_at > datetime.now(timezone.utc)
|
|
)
|
|
|
|
stats = [
|
|
("Total Profiles", total_profiles),
|
|
("Browser-only", session_profiles),
|
|
("System-wide", system_profiles),
|
|
("Active Profiles", active_profiles)
|
|
]
|
|
|
|
for i, (label, value) in enumerate(stats):
|
|
stat_widget = self.create_stat_widget(label, value)
|
|
row, col = divmod(i, 2)
|
|
layout.addWidget(stat_widget, row, col)
|
|
|
|
return widget
|
|
|
|
def create_system_stats(self):
|
|
widget = QWidget()
|
|
layout = QGridLayout(widget)
|
|
layout.setSpacing(10)
|
|
|
|
config_path = Constants.HV_CONFIG_HOME
|
|
|
|
current_connection = self._prepared_value(
|
|
"current_connection", self.update_status.get_current_connection)
|
|
|
|
if current_connection is not None:
|
|
current_connection = current_connection.capitalize()
|
|
|
|
stats = [
|
|
("Config Path", config_path),
|
|
("Client Version", Constants.HV_CLIENT_VERSION_NUMBER),
|
|
("Connection", current_connection),
|
|
]
|
|
|
|
for i, (label, value) in enumerate(stats):
|
|
info_widget = QLabel(f"{label}: {value}")
|
|
info_widget.setStyleSheet(
|
|
f"font-size: 14px; color: white; padding: 5px; {self.font_style}")
|
|
info_widget.setWordWrap(True)
|
|
layout.addWidget(info_widget, i, 0)
|
|
|
|
return widget
|
|
|
|
def create_stat_widget(self, label, value):
|
|
widget = QFrame()
|
|
widget.setStyleSheet(f"""
|
|
QFrame {{
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
{self.font_style}
|
|
}}
|
|
""")
|
|
layout = QVBoxLayout(widget)
|
|
|
|
value_label = QLabel(str(value))
|
|
value_label.setObjectName("value_label")
|
|
value_label.setStyleSheet(
|
|
"font-family: 'Retro Gaming', sans-serif; color: #00ffff; font-size: 22px; font-weight: bold")
|
|
value_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
desc_label = QLabel(label)
|
|
desc_label.setStyleSheet(
|
|
f"color: white; font-size: 12px; {self.font_style}")
|
|
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
layout.addWidget(value_label)
|
|
layout.addWidget(desc_label)
|
|
return widget
|
|
|
|
def create_horizontal_stat(self, label, value, total):
|
|
widget = QWidget()
|
|
layout = QHBoxLayout(widget)
|
|
layout.setContentsMargins(0, 5, 0, 5)
|
|
|
|
text_label = QLabel(f"{label}")
|
|
text_label.setStyleSheet(
|
|
f"color: white; font-size: 12px; {self.font_style}")
|
|
text_label.setFixedWidth(100)
|
|
|
|
progress = QFrame()
|
|
progress.setStyleSheet("""
|
|
QFrame {
|
|
background: rgba(0, 255, 255, 0.3);
|
|
border-radius: 3px;
|
|
}
|
|
""")
|
|
percentage = (value / total) * 100 if total > 0 else 0
|
|
progress.setFixedSize(int(200 * (percentage / 100)), 20)
|
|
|
|
value_label = QLabel(f"{value} ({percentage:.1f}%)")
|
|
value_label.setStyleSheet(
|
|
f"color: white; font-size: 12px; {self.font_style}")
|
|
value_label.setAlignment(Qt.AlignmentFlag.AlignRight)
|
|
|
|
progress_container = QFrame()
|
|
progress_container.setStyleSheet("""
|
|
QFrame {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 3px;
|
|
}
|
|
""")
|
|
progress_container.setFixedSize(200, 20)
|
|
progress_layout = QHBoxLayout(progress_container)
|
|
progress_layout.setContentsMargins(0, 0, 0, 0)
|
|
progress_layout.addWidget(progress)
|
|
progress.setFixedHeight(20)
|
|
|
|
layout.addWidget(text_label)
|
|
layout.addWidget(progress_container)
|
|
layout.addWidget(value_label)
|
|
layout.addStretch()
|
|
|
|
return widget
|
|
|
|
def setup_pages(self):
|
|
self.account_page = self.create_account_page()
|
|
self.subscription_page = self.create_subscription_page()
|
|
self.tickets_page = self.create_tickets_page()
|
|
self.registrations_page = self.create_registrations_page()
|
|
self.verification_page = self.create_verification_page()
|
|
self.systemwide_page = self.create_systemwide_page()
|
|
self.bwrap_page = self.create_bwrap_page()
|
|
self.logs_page = self.create_logs_page()
|
|
self.delete_page = self.create_delete_page()
|
|
self.debug_page = self.create_debug_page()
|
|
|
|
self.content_layout.addWidget(self.account_page)
|
|
self.content_layout.addWidget(self.subscription_page)
|
|
self.content_layout.addWidget(self.tickets_page)
|
|
self.content_layout.addWidget(self.registrations_page)
|
|
self.content_layout.addWidget(self.verification_page)
|
|
self.content_layout.addWidget(self.systemwide_page)
|
|
self.content_layout.addWidget(self.bwrap_page)
|
|
self.content_layout.addWidget(self.logs_page)
|
|
self.content_layout.addWidget(self.delete_page)
|
|
self.content_layout.addWidget(self.debug_page)
|
|
|
|
self.content_layout.setCurrentIndex(0)
|
|
self.show_account_page()
|
|
|
|
def show_account_page(self):
|
|
core_logger.info("User navigated to Settings -> Overview")
|
|
self.content_layout.setCurrentWidget(self.account_page)
|
|
self._select_menu_button("Overview")
|
|
|
|
def show_subscription_page(self):
|
|
core_logger.info("User navigated to Settings -> Subscriptions")
|
|
self.content_layout.setCurrentWidget(self.subscription_page)
|
|
self._select_menu_button("Subscriptions")
|
|
|
|
def show_tickets_page(self):
|
|
core_logger.info("User navigated to Settings -> Tickets")
|
|
self.content_layout.setCurrentWidget(self.tickets_page)
|
|
self._select_menu_button("Tickets")
|
|
self._load_random_tickets_state()
|
|
self._refresh_tickets_inventory()
|
|
self._refresh_ticket_recovery_controls()
|
|
|
|
def _load_random_tickets_state(self):
|
|
if getattr(self, '_random_tickets_state_loaded', False):
|
|
return
|
|
try:
|
|
which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer)
|
|
checked = which_ticket is not None and which_ticket != 'error'
|
|
except Exception:
|
|
checked = False
|
|
self.random_tickets_checkbox.blockSignals(True)
|
|
self.random_tickets_checkbox.setChecked(checked)
|
|
self.random_tickets_checkbox.blockSignals(False)
|
|
self._random_tickets_state_loaded = True
|
|
|
|
def show_registrations_page(self):
|
|
core_logger.info("User navigated to Settings -> Create/Edit")
|
|
self.content_layout.setCurrentWidget(self.registrations_page)
|
|
self._select_menu_button("Create/Edit")
|
|
|
|
def show_verification_page(self):
|
|
core_logger.info("User navigated to Settings -> Verification")
|
|
self.content_layout.setCurrentWidget(self.verification_page)
|
|
self._select_menu_button("Verification")
|
|
|
|
def show_systemwide_page(self):
|
|
core_logger.info("User navigated to Settings -> Legacy-Version")
|
|
self.content_layout.setCurrentWidget(self.systemwide_page)
|
|
self._select_menu_button("Legacy-Version")
|
|
|
|
def show_bwrap_page(self):
|
|
core_logger.info("User navigated to Settings -> Bwrap Permission")
|
|
self.content_layout.setCurrentWidget(self.bwrap_page)
|
|
self._select_menu_button("Bwrap Permission")
|
|
|
|
def show_logs_page(self):
|
|
core_logger.info("User navigated to Settings -> Error Logs")
|
|
self.content_layout.setCurrentWidget(self.logs_page)
|
|
self._select_menu_button("Error Logs")
|
|
|
|
def show_delete_page(self):
|
|
core_logger.info("User navigated to Settings -> Delete Profile")
|
|
self.content_layout.setCurrentWidget(self.delete_page)
|
|
self._select_menu_button("Delete Profile")
|
|
|
|
def show_debug_page(self):
|
|
core_logger.info("User navigated to Settings -> Debug Help")
|
|
self.content_layout.setCurrentWidget(self.debug_page)
|
|
self._select_menu_button("Debug Help")
|
|
|
|
def _select_menu_button(self, label):
|
|
for btn in self.menu_buttons:
|
|
if btn.text() == label:
|
|
btn.setChecked(True)
|
|
else:
|
|
btn.setChecked(False)
|
|
|
|
def create_tickets_page(self):
|
|
page = QWidget()
|
|
page_layout = QVBoxLayout(page)
|
|
page_layout.setSpacing(15)
|
|
page_layout.setContentsMargins(20, 20, 20, 20)
|
|
|
|
title = QLabel("TICKETS")
|
|
title.setStyleSheet(
|
|
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
page_layout.addWidget(title)
|
|
|
|
scroll_area = QScrollArea()
|
|
scroll_area.setWidgetResizable(True)
|
|
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
scroll_area.setStyleSheet("""
|
|
QScrollArea {
|
|
background-color: transparent;
|
|
border: none;
|
|
}
|
|
QScrollArea > QWidget > QWidget {
|
|
background-color: transparent;
|
|
}
|
|
""")
|
|
|
|
scroll_content = QWidget()
|
|
layout = QVBoxLayout(scroll_content)
|
|
layout.setSpacing(15)
|
|
layout.setContentsMargins(0, 0, 8, 0)
|
|
|
|
random_group = QGroupBox("Random Ticket Use")
|
|
random_group.setStyleSheet(
|
|
f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}")
|
|
random_layout = QVBoxLayout(random_group)
|
|
|
|
self.random_tickets_checkbox = QCheckBox("Use a random ticket automatically when enabling a profile")
|
|
self.random_tickets_checkbox.setStyleSheet(f"color: white; {self.font_style}")
|
|
self.random_tickets_checkbox.setChecked(False)
|
|
self.random_tickets_checkbox.toggled.connect(self._on_random_toggle)
|
|
self._random_tickets_state_loaded = False
|
|
random_layout.addWidget(self.random_tickets_checkbox)
|
|
layout.addWidget(random_group)
|
|
|
|
inventory_group = QGroupBox("Unused Tickets")
|
|
inventory_group.setStyleSheet(
|
|
f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}")
|
|
inventory_layout = QVBoxLayout(inventory_group)
|
|
|
|
self.tickets_inventory_label = QLabel("Loading tickets...")
|
|
self.tickets_inventory_label.setStyleSheet(
|
|
f"color: white; font-size: 13px; {self.font_style}")
|
|
self.tickets_inventory_label.setWordWrap(True)
|
|
inventory_layout.addWidget(self.tickets_inventory_label)
|
|
|
|
self.refresh_tickets_button = QPushButton("Refresh")
|
|
self.refresh_tickets_button.setFixedSize(120, 32)
|
|
self.refresh_tickets_button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background: #00aaff; color: white; border: none;
|
|
border-radius: 5px; font-weight: bold; {self.font_style}
|
|
}}
|
|
QPushButton:hover {{ background: #0088cc; }}
|
|
""")
|
|
self.refresh_tickets_button.clicked.connect(self._refresh_tickets_inventory)
|
|
inventory_layout.addWidget(self.refresh_tickets_button)
|
|
layout.addWidget(inventory_group)
|
|
|
|
saved_failure = self._prepared_value(
|
|
"ticket_failure", self.update_status.get_ticket_verification_failure)
|
|
has_failure = saved_failure is not None
|
|
|
|
recovery_group = QGroupBox("Verification Failure Recovery")
|
|
recovery_group.setStyleSheet(
|
|
f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}")
|
|
recovery_layout = QVBoxLayout(recovery_group)
|
|
|
|
self.ticket_recovery_status_label = QLabel(settings_data.format_ticket_failure_status(saved_failure))
|
|
self.ticket_recovery_status_label.setStyleSheet(
|
|
f"color: white; font-size: 12px; {self.font_style}")
|
|
self.ticket_recovery_status_label.setWordWrap(True)
|
|
recovery_layout.addWidget(self.ticket_recovery_status_label)
|
|
|
|
self.evaluate_public_key_button = QPushButton("Evaluate Public Key")
|
|
self.evaluate_public_key_button.setFixedSize(180, 36)
|
|
self.evaluate_public_key_button.setEnabled(has_failure)
|
|
self.evaluate_public_key_button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background: #00aaff; color: white; border: none;
|
|
border-radius: 5px; font-weight: bold; {self.font_style}
|
|
}}
|
|
QPushButton:hover:!disabled {{ background: #0088cc; }}
|
|
QPushButton:disabled {{ background: #666666; color: #bbbbbb; }}
|
|
""")
|
|
self.evaluate_public_key_button.clicked.connect(self.evaluate_ticket_public_key)
|
|
recovery_layout.addWidget(self.evaluate_public_key_button)
|
|
|
|
self.ticket_recovery_output = TerminalWidget()
|
|
self.ticket_recovery_output.setFixedHeight(130)
|
|
if not has_failure:
|
|
self.ticket_recovery_output.setPlainText("No saved verification failure.")
|
|
recovery_layout.addWidget(self.ticket_recovery_output)
|
|
|
|
layout.addWidget(recovery_group)
|
|
|
|
debug_group = QGroupBox("Debug Recovery")
|
|
debug_group.setStyleSheet(
|
|
f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}")
|
|
debug_layout = QVBoxLayout(debug_group)
|
|
|
|
self.prepare_saved_blind_sigs_button = QPushButton(
|
|
"Prepare Tickets even if validation of the server's signature failed")
|
|
self.prepare_saved_blind_sigs_button.setFixedSize(500, 40)
|
|
self.prepare_saved_blind_sigs_button.setEnabled(has_failure)
|
|
self.prepare_saved_blind_sigs_button.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background: #c0392b; color: white; border: none;
|
|
border-radius: 5px; font-size: 10px; font-weight: bold; {self.font_style}
|
|
}}
|
|
QPushButton:hover:!disabled {{ background: #a93226; }}
|
|
QPushButton:disabled {{ background: #666666; color: #bbbbbb; }}
|
|
""")
|
|
self.prepare_saved_blind_sigs_button.clicked.connect(self.prepare_tickets_with_saved_blind_signatures)
|
|
debug_layout.addWidget(self.prepare_saved_blind_sigs_button)
|
|
|
|
layout.addWidget(debug_group)
|
|
layout.addStretch()
|
|
scroll_area.setWidget(scroll_content)
|
|
page_layout.addWidget(scroll_area)
|
|
return page
|
|
|
|
def _refresh_ticket_recovery_controls(self):
|
|
failure = self.update_status.get_ticket_verification_failure()
|
|
has_failure = failure is not None
|
|
if hasattr(self, 'ticket_recovery_status_label'):
|
|
self.ticket_recovery_status_label.setText(settings_data.format_ticket_failure_status(failure))
|
|
if hasattr(self, 'evaluate_public_key_button'):
|
|
self.evaluate_public_key_button.setEnabled(has_failure)
|
|
if hasattr(self, 'prepare_saved_blind_sigs_button'):
|
|
self.prepare_saved_blind_sigs_button.setEnabled(has_failure)
|
|
|
|
def _set_ticket_recovery_busy(self, busy):
|
|
if hasattr(self, 'evaluate_public_key_button'):
|
|
self.evaluate_public_key_button.setEnabled(not busy and self.update_status.get_ticket_verification_failure() is not None)
|
|
if hasattr(self, 'prepare_saved_blind_sigs_button'):
|
|
self.prepare_saved_blind_sigs_button.setEnabled(not busy and self.update_status.get_ticket_verification_failure() is not None)
|
|
|
|
def _write_ticket_recovery_output(self, text):
|
|
if hasattr(self, 'ticket_recovery_output'):
|
|
self.ticket_recovery_output.setPlainText(text)
|
|
|
|
def _get_ticket_failure_for_action(self):
|
|
failure = self.update_status.get_ticket_verification_failure()
|
|
if failure is None:
|
|
self._write_ticket_recovery_output("No saved verification failure.")
|
|
self.update_status.update_status("No ticket verification failure is saved.")
|
|
self._refresh_ticket_recovery_controls()
|
|
return None
|
|
return failure
|
|
|
|
def evaluate_ticket_public_key(self):
|
|
core_logger.info("User clicked 'Evaluate Public Key' button")
|
|
failure = self._get_ticket_failure_for_action()
|
|
if failure is None:
|
|
return
|
|
failed_validations = list(failure.get("failed_validations", []))
|
|
self._write_ticket_recovery_output("Evaluating public key...")
|
|
self.update_status.update_status("Evaluating ticket public key...")
|
|
self._set_ticket_recovery_busy(True)
|
|
|
|
self.ticket_recovery_worker = TicketingWorkerThread(
|
|
'EVALUATE_FAILED_VERIFICATION',
|
|
params={'failed_validations': failed_validations},
|
|
)
|
|
self.ticket_recovery_worker.failed_verification_evaluated.connect(self.on_failed_verification_evaluated)
|
|
self.ticket_recovery_worker.error.connect(self.on_ticket_recovery_error)
|
|
self.ticket_recovery_worker.start()
|
|
|
|
def on_failed_verification_evaluated(self, result):
|
|
message = interpret_key_results(result)
|
|
self._write_ticket_recovery_output(message)
|
|
core_logger.info(f"Public key evaluation result: {message}")
|
|
self.update_status.update_status("Ticket public key evaluation complete.")
|
|
self._set_ticket_recovery_busy(False)
|
|
self._refresh_ticket_recovery_controls()
|
|
|
|
def prepare_tickets_with_saved_blind_signatures(self):
|
|
core_logger.info("User clicked 'Prepare Tickets even if validation of the server's signature failed' button")
|
|
failure = self._get_ticket_failure_for_action()
|
|
if failure is None:
|
|
return
|
|
|
|
reply = QMessageBox.warning(
|
|
self,
|
|
"Prepare Tickets Anyway",
|
|
"This will prepare tickets even though server signature validation failed. Continue only if you have evaluated the situation.",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
QMessageBox.StandardButton.No,
|
|
)
|
|
if reply != QMessageBox.StandardButton.Yes:
|
|
return
|
|
|
|
self._write_ticket_recovery_output("Preparing tickets with saved blind signatures...")
|
|
self.update_status.update_status("Preparing tickets with saved blind signatures...")
|
|
self._set_ticket_recovery_busy(True)
|
|
|
|
self.ticket_recovery_worker = TicketingWorkerThread('PREPARE_SAVED_BLIND_SIGS')
|
|
self.ticket_recovery_worker.saved_blind_prep_done.connect(self.on_saved_blind_prep_done)
|
|
self.ticket_recovery_worker.error.connect(self.on_ticket_recovery_error)
|
|
self.ticket_recovery_worker.start()
|
|
|
|
def on_saved_blind_prep_done(self, result):
|
|
self._write_ticket_recovery_output(settings_data.format_ticket_recovery_result("Results of preparation", result))
|
|
if isinstance(result, dict) and result.get('valid') is True:
|
|
self.update_status.clear_ticket_verification_failure()
|
|
self.update_status.update_status("Tickets prepared from saved blind signatures.")
|
|
self._refresh_tickets_inventory()
|
|
else:
|
|
msg = result.get('message', 'failed') if isinstance(result, dict) else 'failed'
|
|
self.update_status.update_status(f"Saved blind signature prep failed: {msg}")
|
|
self._set_ticket_recovery_busy(False)
|
|
self._refresh_ticket_recovery_controls()
|
|
|
|
def on_ticket_recovery_error(self, msg):
|
|
core_logger.error(f"Ticket recovery error: {msg}")
|
|
self._write_ticket_recovery_output(f"EXCEPTION: {msg}")
|
|
self.update_status.update_status(f"Ticket recovery error: {msg}")
|
|
self._set_ticket_recovery_busy(False)
|
|
self._refresh_ticket_recovery_controls()
|
|
|
|
def _on_random_toggle(self, checked):
|
|
core_logger.info(f"User toggled random ticket setting to {'on' if checked else 'off'}")
|
|
try:
|
|
result = modify_random_tickets_setting('on' if checked else 'off', ticket_observer)
|
|
if not (isinstance(result, dict) and result.get('valid')):
|
|
msg = result.get('message', 'failed') if isinstance(result, dict) else 'failed'
|
|
core_logger.error(f"Could not change random ticket setting: {msg}")
|
|
self.update_status.update_status(f'Could not change setting: {msg}')
|
|
except Exception as e:
|
|
core_logger.error(f"Exception changing random ticket setting: {e}")
|
|
self.update_status.update_status(f'Could not change setting: {e}')
|
|
|
|
def _refresh_tickets_inventory(self):
|
|
if not hasattr(self, 'tickets_inventory_label'):
|
|
return
|
|
core_logger.info("Refreshing tickets inventory")
|
|
try:
|
|
unused = get_unused_tickets(ticket_observer)
|
|
except Exception as e:
|
|
core_logger.error(f"Error fetching unused tickets: {e}")
|
|
self.tickets_inventory_label.setText(f"Error: {e}")
|
|
return
|
|
if isinstance(unused, dict) and unused.get('valid'):
|
|
data = unused.get('data', [])
|
|
if data:
|
|
self.tickets_inventory_label.setText(
|
|
f"You have {len(data)} unused tickets: " + ", ".join(f"#{t}" for t in data))
|
|
else:
|
|
self.tickets_inventory_label.setText("No unused tickets.")
|
|
else:
|
|
msg = unused.get('message', 'No tickets found.') if isinstance(unused, dict) else 'No tickets found.'
|
|
self.tickets_inventory_label.setText(msg)
|
|
|
|
def create_logs_page(self):
|
|
page = QWidget()
|
|
layout = QVBoxLayout(page)
|
|
layout.setSpacing(20)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
|
|
title = QLabel("LOGGING SETTINGS")
|
|
title.setStyleSheet(
|
|
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
layout.addWidget(title)
|
|
|
|
log_text = QLabel(
|
|
f"Enabling this feature means error logs will be stored on your local\nmachine at: '{Constants.HV_CACHE_HOME}/gui'.")
|
|
log_text.setStyleSheet(
|
|
f"color: white; font-size: 14px; {self.font_style}")
|
|
layout.addWidget(log_text)
|
|
|
|
logs_group = QGroupBox("Log Configuration")
|
|
logs_group.setStyleSheet(
|
|
f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}")
|
|
logs_layout = QVBoxLayout(logs_group)
|
|
|
|
self.enable_gui_logging = QCheckBox("Enable GUI logging")
|
|
self.enable_gui_logging.setStyleSheet(self.get_checkbox_style())
|
|
logs_layout.addWidget(self.enable_gui_logging)
|
|
layout.addWidget(logs_group)
|
|
|
|
save_button = QPushButton()
|
|
save_button.setFixedSize(75, 46)
|
|
save_button.setIcon(QIcon(os.path.join(self.btn_path, f"save.png")))
|
|
save_button.setIconSize(QSize(75, 46))
|
|
save_button.clicked.connect(self.save_logs_settings)
|
|
|
|
button_layout = QHBoxLayout()
|
|
button_layout.addStretch()
|
|
button_layout.addWidget(save_button)
|
|
layout.addLayout(button_layout)
|
|
|
|
layout.addStretch()
|
|
self.load_logs_settings()
|
|
return page
|
|
|
|
def load_logs_settings(self) -> None:
|
|
try:
|
|
config = self._prepared_value(
|
|
"gui_config", self.update_status._load_gui_config)
|
|
if config and "logging" in config:
|
|
self.enable_gui_logging.setChecked(
|
|
config["logging"]["gui_logging_enabled"])
|
|
except Exception as e:
|
|
logging.error(f"Error loading logging settings: {str(e)}")
|
|
self.enable_gui_logging.setChecked(True)
|
|
|
|
def save_logs_settings(self) -> None:
|
|
try:
|
|
config = self.update_status._load_gui_config()
|
|
if config is None:
|
|
config = {
|
|
"logging": {
|
|
"gui_logging_enabled": True,
|
|
"log_level": "INFO"
|
|
}
|
|
}
|
|
|
|
config["logging"]["gui_logging_enabled"] = self.enable_gui_logging.isChecked()
|
|
|
|
self.update_status._save_gui_config(config)
|
|
|
|
if self.enable_gui_logging.isChecked():
|
|
self.update_status._setup_gui_logging()
|
|
core_logger.info("User enabled GUI logging")
|
|
else:
|
|
core_logger.info("User disabled GUI logging")
|
|
self.update_status.stop_gui_logging()
|
|
|
|
self.update_status.update_status(
|
|
"Logging settings saved successfully")
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error saving logging settings: {str(e)}")
|
|
self.update_status.update_status("Error saving logging settings")
|
|
|
|
def create_registrations_page(self):
|
|
page = QWidget()
|
|
layout = QVBoxLayout(page)
|
|
layout.setSpacing(20)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
|
|
title = QLabel("Create/Edit SETTINGS")
|
|
title.setStyleSheet(
|
|
f"color: #808080; font-size: 15px; font-weight: bold; {self.font_style}")
|
|
layout.addWidget(title)
|
|
|
|
registrations_group = QGroupBox("Create/Edit Configuration")
|
|
registrations_group.setStyleSheet(
|
|
f"QGroupBox {{ color: white; font-size: 15px; padding: 15px; {self.font_style} }}")
|
|
registrations_layout = QVBoxLayout(registrations_group)
|
|
|
|
self.enable_auto_sync = QCheckBox("Enable auto-sync on Edit")
|
|
self.enable_auto_sync.setStyleSheet(self.get_checkbox_style())
|
|
registrations_layout.addWidget(self.enable_auto_sync)
|
|
|
|
self.enable_fast_registration = QCheckBox(
|
|
"Enable Fast-mode for Profile Creation")
|
|
self.enable_fast_registration.setStyleSheet(self.get_checkbox_style())
|
|
registrations_layout.addWidget(self.enable_fast_registration)
|
|
|
|
self.dynamic_profiles_container = QWidget()
|
|
dynamic_layout = QVBoxLayout(self.dynamic_profiles_container)
|
|
dynamic_layout.setContentsMargins(20, 0, 0, 0)
|
|
|
|
self.enable_dynamic_profiles = QCheckBox(
|
|
"Enable Dynamic Larger Profiles")
|
|
self.enable_dynamic_profiles.setStyleSheet(self.get_checkbox_style())
|
|
dynamic_layout.addWidget(self.enable_dynamic_profiles)
|
|
|
|
dynamic_desc = QLabel("This is for fast mode profile creation. Some users may wish to have screen sizes that are nearly as large as their host, but slightly smaller by differing amounts. This fools fingerprinters but still gives larger sizes. So for example if your host is 1200 x 800, then fast mode would offer 1150 x 750, 1100 x 700, 1050 x 650, etc.")
|
|
dynamic_desc.setWordWrap(True)
|
|
dynamic_desc.setStyleSheet(
|
|
f"color: #888888; font-size: 11px; font-style: italic; {self.font_style}")
|
|
dynamic_layout.addWidget(dynamic_desc)
|
|
|
|
registrations_layout.addWidget(self.dynamic_profiles_container)
|
|
|
|
self.enable_fast_registration.toggled.connect(
|
|
lambda checked: self.dynamic_profiles_container.setVisible(checked))
|
|
|
|
layout.addWidget(registrations_group)
|
|
|
|
save_button = QPushButton()
|
|
save_button.setFixedSize(75, 46)
|
|
save_button.setIcon(QIcon(os.path.join(self.btn_path, f"save.png")))
|
|
save_button.setIconSize(QSize(75, 46))
|
|
save_button.clicked.connect(self.save_registrations_settings)
|
|
|
|
button_layout = QHBoxLayout()
|
|
button_layout.addWidget(save_button)
|
|
button_layout.addStretch()
|
|
layout.addLayout(button_layout)
|
|
|
|
layout.addStretch()
|
|
self.load_registrations_settings()
|
|
return page
|
|
|
|
def create_systemwide_page(self):
|
|
page = QWidget()
|
|
layout = QVBoxLayout(page)
|
|
layout.setSpacing(20)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
|
|
title = QLabel("SYSTEM-WIDE PROFILES")
|
|
title.setStyleSheet(
|
|
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
layout.addWidget(title)
|
|
|
|
description = QLabel(
|
|
"This is for the old version of the app! The new version prior to 2.1.0 does not need it. This allows you to enable or disable a legacy systemwide policy for sudo.")
|
|
description.setWordWrap(True)
|
|
description.setStyleSheet(
|
|
f"color: white; font-size: 14px; {self.font_style}")
|
|
layout.addWidget(description)
|
|
|
|
status_layout = QHBoxLayout()
|
|
status_label = QLabel("Current status:")
|
|
status_label.setStyleSheet(
|
|
f"color: white; font-size: 14px; {self.font_style}")
|
|
self.systemwide_status_value = QLabel("")
|
|
self.systemwide_status_value.setStyleSheet(
|
|
f"color: #e67e22; font-size: 14px; {self.font_style}")
|
|
status_layout.addWidget(status_label)
|
|
status_layout.addWidget(self.systemwide_status_value)
|
|
status_layout.addStretch()
|
|
layout.addLayout(status_layout)
|
|
|
|
toggle_layout = QHBoxLayout()
|
|
self.systemwide_toggle = QCheckBox(
|
|
"Enable \"No Sudo\" Systemwide Policy")
|
|
self.systemwide_toggle.setStyleSheet(self.get_checkbox_style())
|
|
toggle_layout.addWidget(self.systemwide_toggle)
|
|
toggle_layout.addStretch()
|
|
layout.addLayout(toggle_layout)
|
|
|
|
save_button = QPushButton()
|
|
save_button.setFixedSize(75, 46)
|
|
save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png")))
|
|
save_button.setIconSize(QSize(75, 46))
|
|
save_button.clicked.connect(self.save_systemwide_settings)
|
|
|
|
button_layout = QHBoxLayout()
|
|
button_layout.addWidget(save_button)
|
|
button_layout.addStretch()
|
|
layout.addLayout(button_layout)
|
|
|
|
layout.addStretch()
|
|
self.load_systemwide_settings(
|
|
self._prepared_value("systemwide_enabled", settings_data.read_systemwide_enabled))
|
|
return page
|
|
|
|
def create_bwrap_page(self):
|
|
page = QWidget()
|
|
layout = QVBoxLayout(page)
|
|
layout.setSpacing(20)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
|
|
title = QLabel("BWRAP PERMISSION")
|
|
title.setStyleSheet(
|
|
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
|
|
layout.addWidget(title)
|
|
|
|
description = QLabel(
|
|
"Control whether HydraVeil configures a capability policy so bwrap can be used without requiring additional permissions.")
|
|
description.setWordWrap(True)
|
|
description.setStyleSheet(
|
|
f"color: white; font-size: 14px; {self.font_style}")
|
|
layout.addWidget(description)
|
|
|
|
status_layout = QHBoxLayout()
|
|
status_label = QLabel("Current status:")
|
|
status_label.setStyleSheet(
|
|
f"color: white; font-size: 14px; {self.font_style}")
|
|
self.bwrap_status_value = QLabel("")
|
|
self.bwrap_status_value.setStyleSheet(
|
|
f"color: #e67e22; font-size: 14px; {self.font_style}")
|
|
status_layout.addWidget(status_label)
|
|
status_layout.addWidget(self.bwrap_status_value)
|
|
status_layout.addStretch()
|
|
layout.addLayout(status_layout)
|
|
|
|
toggle_layout = QHBoxLayout()
|
|
self.bwrap_toggle = QCheckBox("Enable capability policy")
|
|
self.bwrap_toggle.setStyleSheet(self.get_checkbox_style())
|
|
toggle_layout.addWidget(self.bwrap_toggle)
|
|
toggle_layout.addStretch()
|
|
layout.addLayout(toggle_layout)
|
|
|
|
save_button = QPushButton()
|
|
save_button.setFixedSize(75, 46)
|
|
save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png")))
|
|
save_button.setIconSize(QSize(75, 46))
|
|
save_button.clicked.connect(self.save_bwrap_settings)
|
|
|
|
button_layout = QHBoxLayout()
|
|
button_layout.addWidget(save_button)
|
|
button_layout.addStretch()
|
|
layout.addLayout(button_layout)
|
|
|
|
layout.addStretch()
|
|
self.load_bwrap_settings(
|
|
self._prepared_value("bwrap_enabled", settings_data.read_bwrap_enabled))
|
|
return page
|
|
|
|
def load_systemwide_settings(self, enabled=None):
|
|
if enabled is None:
|
|
enabled = settings_data.read_systemwide_enabled()
|
|
self.systemwide_toggle.setChecked(enabled)
|
|
if enabled:
|
|
self.systemwide_status_value.setText("Enabled")
|
|
self.systemwide_status_value.setStyleSheet(
|
|
f"color: #2ecc71; font-size: 14px; {self.font_style}")
|
|
else:
|
|
self.systemwide_status_value.setText("Disabled")
|
|
self.systemwide_status_value.setStyleSheet(
|
|
f"color: #e67e22; font-size: 14px; {self.font_style}")
|
|
|
|
def save_systemwide_settings(self):
|
|
enable = self.systemwide_toggle.isChecked()
|
|
try:
|
|
privilege_policy = PolicyController.get('privilege')
|
|
if privilege_policy is not None:
|
|
if enable:
|
|
PolicyController.instate(privilege_policy)
|
|
else:
|
|
if PolicyController.is_instated(privilege_policy):
|
|
PolicyController.revoke(privilege_policy)
|
|
self.load_systemwide_settings()
|
|
self.update_status.update_status(
|
|
"System-wide policy settings updated")
|
|
except CommandNotFoundError as e:
|
|
self.systemwide_status_value.setText(str(e))
|
|
self.systemwide_status_value.setStyleSheet(
|
|
f"color: red; font-size: 14px; {self.font_style}")
|
|
except (PolicyAssignmentError, PolicyInstatementError, PolicyRevocationError) as e:
|
|
self.systemwide_status_value.setText(str(e))
|
|
self.systemwide_status_value.setStyleSheet(
|
|
f"color: red; font-size: 14px; {self.font_style}")
|
|
except Exception:
|
|
self.systemwide_status_value.setText("Failed to update policy")
|
|
self.systemwide_status_value.setStyleSheet(
|
|
f"color: red; font-size: 14px; {self.font_style}")
|
|
|
|
def load_bwrap_settings(self, enabled=None):
|
|
if enabled is None:
|
|
enabled = settings_data.read_bwrap_enabled()
|
|
self.bwrap_toggle.setChecked(enabled)
|
|
if enabled:
|
|
self.bwrap_status_value.setText("Enabled")
|
|
self.bwrap_status_value.setStyleSheet(
|
|
f"color: #2ecc71; font-size: 14px; {self.font_style}")
|
|
else:
|
|
self.bwrap_status_value.setText("Disabled")
|
|
self.bwrap_status_value.setStyleSheet(
|
|
f"color: #e67e22; font-size: 14px; {self.font_style}")
|
|
|
|
def save_bwrap_settings(self):
|
|
enable = self.bwrap_toggle.isChecked()
|
|
try:
|
|
capability_policy = PolicyController.get('capability')
|
|
if capability_policy is not None:
|
|
if enable:
|
|
PolicyController.instate(capability_policy)
|
|
else:
|
|
if PolicyController.is_instated(capability_policy):
|
|
PolicyController.revoke(capability_policy)
|
|
self.load_bwrap_settings()
|
|
self.update_status.update_status(
|
|
"Capability policy settings updated")
|
|
except CommandNotFoundError as e:
|
|
self.bwrap_status_value.setText(str(e))
|
|
self.bwrap_status_value.setStyleSheet(
|
|
f"color: red; font-size: 14px; {self.font_style}")
|
|
except (PolicyAssignmentError, PolicyInstatementError, PolicyRevocationError) as e:
|
|
self.bwrap_status_value.setText(str(e))
|
|
self.bwrap_status_value.setStyleSheet(
|
|
f"color: red; font-size: 14px; {self.font_style}")
|
|
except Exception:
|
|
self.bwrap_status_value.setText("Failed to update policy")
|
|
self.bwrap_status_value.setStyleSheet(
|
|
f"color: red; font-size: 14px; {self.font_style}")
|
|
|
|
def load_registrations_settings(self) -> None:
|
|
try:
|
|
config = self._prepared_value(
|
|
"gui_config", self.update_status._load_gui_config)
|
|
if config and "registrations" in config:
|
|
registrations = config["registrations"]
|
|
auto_sync = registrations.get("auto_sync_enabled", False)
|
|
fast_reg = registrations.get(
|
|
"fast_registration_enabled", False)
|
|
dynamic_profiles = registrations.get(
|
|
"dynamic_larger_profiles", False)
|
|
if "auto_sync_enabled" not in registrations or "fast_registration_enabled" not in registrations or "dynamic_larger_profiles" not in registrations:
|
|
registrations["auto_sync_enabled"] = auto_sync
|
|
registrations["fast_registration_enabled"] = fast_reg
|
|
registrations["dynamic_larger_profiles"] = dynamic_profiles
|
|
self.update_status._save_gui_config(config)
|
|
self.enable_auto_sync.setChecked(auto_sync)
|
|
self.enable_fast_registration.setChecked(fast_reg)
|
|
self.enable_dynamic_profiles.setChecked(dynamic_profiles)
|
|
self.dynamic_profiles_container.setVisible(fast_reg)
|
|
else:
|
|
self.enable_auto_sync.setChecked(False)
|
|
self.enable_fast_registration.setChecked(False)
|
|
self.enable_dynamic_profiles.setChecked(False)
|
|
self.dynamic_profiles_container.setVisible(False)
|
|
except Exception as e:
|
|
logging.error(f"Error loading registration settings: {str(e)}")
|
|
self.enable_auto_sync.setChecked(False)
|
|
self.enable_fast_registration.setChecked(False)
|
|
self.enable_dynamic_profiles.setChecked(False)
|
|
self.dynamic_profiles_container.setVisible(False)
|
|
|
|
def save_registrations_settings(self) -> None:
|
|
try:
|
|
config = self.update_status._load_gui_config()
|
|
if config is None:
|
|
config = {
|
|
"logging": {
|
|
"gui_logging_enabled": True,
|
|
"log_level": "INFO"
|
|
},
|
|
"registrations": {
|
|
"auto_sync_enabled": False,
|
|
"fast_registration_enabled": False
|
|
}
|
|
}
|
|
|
|
if "registrations" not in config:
|
|
config["registrations"] = {}
|
|
|
|
config["registrations"]["auto_sync_enabled"] = self.enable_auto_sync.isChecked()
|
|
config["registrations"]["fast_registration_enabled"] = self.enable_fast_registration.isChecked()
|
|
config["registrations"]["dynamic_larger_profiles"] = self.enable_dynamic_profiles.isChecked()
|
|
|
|
self.update_status._save_gui_config(config)
|
|
self.update_status.update_status("Registration settings saved")
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error saving registration settings: {str(e)}")
|
|
self.update_status.update_status(
|
|
"Error saving registration settings")
|