from PyQt6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QMessageBox, QInputDialog, QLineEdit, ) from PyQt6.QtCore import Qt, QTimer from PyQt6.QtGui import QFont, QCloseEvent, QPalette, QColor from app.core.config import Config from app.core import wireguard as wg_core from app.core import mfa as mfa_core from app.ui.mfa_dialog import MFADialog from app.ui.admin_window import AdminWindow from app.ui import icons class MainWindow(QMainWindow): def __init__(self, config: Config, parent=None): super().__init__(parent) self._cfg = config self._connecting = False self.setWindowTitle("WGSecure") self.setFixedSize(400, 520) self.setWindowFlags( Qt.WindowType.Window | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowCloseButtonHint | Qt.WindowType.WindowMinimizeButtonHint ) self.setWindowIcon(icons.icon_app()) self._build_ui() self._status_timer = QTimer(self) self._status_timer.timeout.connect(self._refresh_status) self._status_timer.start(3000) self._refresh_status() # ------------------------------------------------------------------ # # Construction UI # ------------------------------------------------------------------ # def _build_ui(self): central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # -- En-tête -------------------------------------------------- header = QWidget() header.setFixedHeight(72) header.setStyleSheet("background: #2c3e50;") h_layout = QHBoxLayout(header) h_layout.setContentsMargins(16, 10, 16, 10) self._icon_label = QLabel() self._icon_label.setFixedSize(44, 44) self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) h_layout.addWidget(self._icon_label) title_col = QVBoxLayout() title_col.setSpacing(2) app_title = QLabel("WGSecure") app_title.setStyleSheet("color: white; font-size: 17px; font-weight: bold; background: transparent;") version_label = QLabel("v0.1.0 — WireGuard + MFA") version_label.setStyleSheet("color: #95a5a6; font-size: 10px; background: transparent;") title_col.addWidget(app_title) title_col.addWidget(version_label) h_layout.addLayout(title_col) h_layout.addStretch() layout.addWidget(header) # -- Zone statut ---------------------------------------------- status_area = QWidget() status_area.setFixedHeight(90) status_area.setStyleSheet("background: #ecf0f1;") s_layout = QVBoxLayout(status_area) s_layout.setContentsMargins(10, 10, 10, 10) s_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) self._status_badge = QLabel("● Déconnecté") self._status_badge.setAlignment(Qt.AlignmentFlag.AlignCenter) self._status_badge.setStyleSheet("color: #e74c3c; font-size: 16px; font-weight: bold; background: transparent;") s_layout.addWidget(self._status_badge) self._status_sub = QLabel("") self._status_sub.setAlignment(Qt.AlignmentFlag.AlignCenter) self._status_sub.setStyleSheet("color: #7f8c8d; font-size: 11px; background: transparent;") s_layout.addWidget(self._status_sub) layout.addWidget(status_area) # -- Séparateur ----------------------------------------------- sep = QFrame() sep.setFrameShape(QFrame.Shape.HLine) sep.setStyleSheet("color: #bdc3c7;") layout.addWidget(sep) # -- Section infos -------------------------------------------- info_frame = QFrame() info_frame.setFrameShape(QFrame.Shape.NoFrame) info_frame.setStyleSheet("background-color: #ffffff;") info_frame.setAutoFillBackground(True) i_layout = QVBoxLayout(info_frame) i_layout.setContentsMargins(16, 12, 16, 12) i_layout.setSpacing(8) self._info_labels: dict[str, QLabel] = {} rows = [ ("server", "Serveur"), ("iface", "Interface"), ("addr", "Adresse IP"), ("mfa", "MFA"), ("rx_tx", "Transfert"), ("handshake", "Dernier handshake"), ] for key, label in rows: row_w = QWidget() row_w.setStyleSheet("background: transparent;") row_layout = QHBoxLayout(row_w) row_layout.setContentsMargins(0, 0, 0, 0) row_layout.setSpacing(8) lbl_key = QLabel(f"{label} :") lbl_key.setFixedWidth(130) lbl_key.setStyleSheet("color: #7f8c8d; font-size: 12px; background: transparent;") lbl_val = QLabel("—") lbl_val.setStyleSheet("color: #2c3e50; font-size: 12px; background: transparent;") lbl_val.setWordWrap(True) self._info_labels[key] = lbl_val row_layout.addWidget(lbl_key) row_layout.addWidget(lbl_val, 1) i_layout.addWidget(row_w) layout.addWidget(info_frame, 1) # stretch factor 1 — prend l'espace disponible # -- Séparateur bas ------------------------------------------- sep2 = QFrame() sep2.setFrameShape(QFrame.Shape.HLine) sep2.setStyleSheet("color: #bdc3c7;") layout.addWidget(sep2) # -- Boutons -------------------------------------------------- btn_area = QWidget() btn_area.setStyleSheet("background: #f8f9fa;") b_layout = QVBoxLayout(btn_area) b_layout.setContentsMargins(16, 12, 16, 12) b_layout.setSpacing(8) self._btn_connect = QPushButton("Se connecter") self._btn_connect.setFixedHeight(42) self._btn_connect.setStyleSheet( "QPushButton { background: #27ae60; color: white; font-size: 14px; " "font-weight: bold; border-radius: 6px; border: none; }" "QPushButton:hover { background: #219a52; }" "QPushButton:disabled { background: #bdc3c7; color: #ecf0f1; }" ) self._btn_connect.clicked.connect(self._on_connect) b_layout.addWidget(self._btn_connect) btn_admin = QPushButton("Panneau Administrateur") btn_admin.setFixedHeight(34) btn_admin.setStyleSheet( "QPushButton { background: #dde1e7; color: #2c3e50; border-radius: 6px; " "font-size: 12px; border: none; }" "QPushButton:hover { background: #bdc3c7; color: #2c3e50; }" ) btn_admin.clicked.connect(self._open_admin) b_layout.addWidget(btn_admin) layout.addWidget(btn_area) # ------------------------------------------------------------------ # # Rafraîchissement du statut # ------------------------------------------------------------------ # def _refresh_status(self): wg = self._cfg.wg srv = wg.get("server_endpoint", "") port = wg.get("server_port", "") self._info_labels["server"].setText(f"{srv}:{port}" if srv else "—") self._info_labels["iface"].setText(wg.get("interface_name", "wgs0")) self._info_labels["addr"].setText(wg.get("client_address", "—")) self._info_labels["mfa"].setText( "Activé ✓" if self._cfg.mfa_enabled else "Désactivé" ) try: info = wg_core.get_status_info(self._cfg) connected = info["connected"] except Exception: connected = False info = {} if connected: self._status_badge.setText("● Connecté") self._status_badge.setStyleSheet( "color: #27ae60; font-size: 16px; font-weight: bold; background: transparent;" ) self._btn_connect.setText("Se déconnecter") self._btn_connect.setStyleSheet( "QPushButton { background: #e74c3c; color: white; font-size: 14px; " "font-weight: bold; border-radius: 6px; border: none; }" "QPushButton:hover { background: #c0392b; }" ) self._info_labels["rx_tx"].setText( f"↓ {info.get('rx_bytes','—')} ↑ {info.get('tx_bytes','—')}" ) self._info_labels["handshake"].setText(info.get("last_handshake", "—")) self._status_sub.setText(f"Interface active : {info.get('interface','—')}") self.setWindowIcon(icons.icon_connected()) else: self._status_badge.setText("● Déconnecté") self._status_badge.setStyleSheet( "color: #e74c3c; font-size: 16px; font-weight: bold; background: transparent;" ) self._btn_connect.setText("Se connecter") self._btn_connect.setStyleSheet( "QPushButton { background: #27ae60; color: white; font-size: 14px; " "font-weight: bold; border-radius: 6px; border: none; }" "QPushButton:hover { background: #219a52; }" ) self._info_labels["rx_tx"].setText("—") self._info_labels["handshake"].setText("—") self._status_sub.setText("") self.setWindowIcon(icons.icon_disconnected()) pix = (icons.icon_connected() if connected else icons.icon_disconnected()).pixmap(36, 36) self._icon_label.setPixmap(pix) # ------------------------------------------------------------------ # # Actions # ------------------------------------------------------------------ # def _on_connect(self): if self._connecting: return try: connected = wg_core.is_connected(self._cfg) except Exception: connected = False if connected: ok, msg = wg_core.disconnect(self._cfg) if not ok: QMessageBox.warning(self, "Erreur", f"Déconnexion échouée :\n{msg}") else: if not self._cfg.configured: QMessageBox.information( self, "Configuration manquante", "WireGuard n'est pas encore configuré.\n" "Ouvrez le panneau Administrateur pour paramétrer la connexion." ) return if self._cfg.mfa_enabled: if not self._cfg.mfa_secret: QMessageBox.warning( self, "MFA non configuré", "Le MFA est activé mais aucun secret n'est configuré." ) return dlg = MFADialog(self._cfg.mfa_secret, self) if dlg.exec() != MFADialog.DialogCode.Accepted or not dlg.is_verified(): return self._btn_connect.setEnabled(False) self._btn_connect.setText("Connexion…") self._connecting = True from PyQt6.QtWidgets import QApplication QApplication.processEvents() ok, msg = wg_core.connect(self._cfg) self._connecting = False self._btn_connect.setEnabled(True) if not ok: QMessageBox.warning(self, "Erreur de connexion", msg) self._refresh_status() def _open_admin(self): if self._cfg.has_admin_password(): pw, ok = QInputDialog.getText( self, "Accès Administrateur", "Mot de passe administrateur :", QLineEdit.EchoMode.Password, ) if not ok: return if not self._cfg.check_admin_password(pw): QMessageBox.warning(self, "Refusé", "Mot de passe incorrect.") return dlg = AdminWindow(self._cfg, self) dlg.exec() self._refresh_status() def closeEvent(self, event: QCloseEvent): if self._cfg.get("ui", "minimize_to_tray"): event.ignore() self.hide() else: event.accept()