from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget, QLabel, QLineEdit, QPushButton, QSpinBox, QCheckBox, QGroupBox, QFormLayout, QTextEdit, QMessageBox, QFrame, QFileDialog, QSizePolicy, ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QFont from app.core.config import Config from app.core import wireguard as wg_core from app.core import dns as dns_util from app.core import mfa as mfa_core # CSS sombre commun à tous les onglets _TAB_CSS = """ QWidget { background-color: #1c2833; color: white; } QLabel { color: white; background: transparent; } QCheckBox { color: white; } QGroupBox { color: white; border: 1px solid rgba(255,255,255,0.2); border-radius: 5px; margin-top: 10px; font-weight: bold; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 4px; color: white; } QLineEdit { background: rgba(255,255,255,0.1); color: white; border: 1px solid rgba(255,255,255,0.25); border-radius: 4px; padding: 4px 6px; } QLineEdit:read-only { background: rgba(255,255,255,0.06); } QSpinBox { background: rgba(255,255,255,0.1); color: white; border: 1px solid rgba(255,255,255,0.25); border-radius: 4px; padding: 3px 6px; } QSpinBox::up-button, QSpinBox::down-button { background: rgba(255,255,255,0.15); } QTextEdit { background: rgba(255,255,255,0.07); color: #a8d8ea; border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; } QPushButton { background: #2471a3; color: white; border-radius: 5px; padding: 7px 12px; border: none; } QPushButton:hover { background: #1a5276; } QPushButton:checked { background: #154360; } """ _RESULT_CSS = ( "QTextEdit { border: 1px solid rgba(255,255,255,0.12); border-radius: 6px;" " padding: 8px; font-size: 12px; }" "QScrollBar:vertical { background: transparent; width: 8px; border: none; }" "QScrollBar::handle:vertical { background: #2e4057; border-radius: 4px; }" "QScrollBar::add-line:vertical," "QScrollBar::sub-line:vertical { height: 0; }" ) class AdminWindow(QDialog): def __init__(self, config: Config, parent=None): super().__init__(parent) self._cfg = config self.setWindowTitle("WGSecure — Panneau Administrateur") self.setWindowFlags( Qt.WindowType.Dialog | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowCloseButtonHint ) # Taille figée auparavant : le rapport de diagnostic, long d'une # dizaine d'étapes détaillées, était comprimé en une bande illisible. self.setMinimumSize(720, 620) self.resize(880, 740) self._build_ui() self._load_values() # ------------------------------------------------------------------ # # Helper : page sombre avec bandeau coloré # ------------------------------------------------------------------ # def _dark_page(self, banner_text: str, banner_color: str, scroll: bool = False): """Retourne (page_widget, body_layout) : fond sombre + bandeau. `scroll=True` place le corps dans une zone défilante : indispensable pour les pages dont le contenu grandit à l'usage (rapport de diagnostic), sinon les widgets se compriment jusqu'à l'illisible. """ w = QWidget() w.setStyleSheet(_TAB_CSS) outer = QVBoxLayout(w) outer.setContentsMargins(0, 0, 0, 0) outer.setSpacing(0) hdr = QLabel(f" {banner_text}") hdr.setFixedHeight(36) hdr.setStyleSheet( f"background: {banner_color}; color: white; font-size: 13px;" " font-weight: bold; padding-left: 8px;" ) outer.addWidget(hdr) body = QWidget() body_layout = QVBoxLayout(body) body_layout.setContentsMargins(14, 14, 14, 14) body_layout.setSpacing(10) if scroll: from PyQt6.QtWidgets import QScrollArea body.setStyleSheet("background: #1c2833;") area = QScrollArea() area.setWidgetResizable(True) area.setStyleSheet( "QScrollArea { border: none; background: #1c2833; }" "QScrollBar:vertical { background: #1c2833; width: 8px;" " border: none; }" "QScrollBar::handle:vertical { background: #2e4057;" " border-radius: 4px; }" "QScrollBar::add-line:vertical," "QScrollBar::sub-line:vertical { height: 0; }" ) area.setWidget(body) outer.addWidget(area) else: outer.addWidget(body) return w, body_layout # ------------------------------------------------------------------ # # Construction principale # ------------------------------------------------------------------ # def _build_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) banner = QLabel(" WGSecure — Configuration Administrateur") banner.setFixedHeight(40) banner.setStyleSheet( "background: #17202a; color: white; font-size: 14px; font-weight: bold;" ) layout.addWidget(banner) tabs = QTabWidget() tabs.setStyleSheet(""" QTabWidget::pane { border: none; } QTabBar::tab { padding: 8px 16px; background: #1c2833; color: rgba(255,255,255,0.6); border: none; } QTabBar::tab:selected { background: #2e4057; color: white; font-weight: bold; } QTabBar::tab:hover { background: #253545; color: white; } """) tabs.addTab(self._tab_wireguard(), "WireGuard") tabs.addTab(self._tab_keys(), "Clés") tabs.addTab(self._tab_mfa(), "MFA") tabs.addTab(self._tab_test(), "Test connexion") tabs.addTab(self._tab_settings(), "Paramètres") tabs.addTab(self._tab_admin(), "Sécurité") tabs.addTab(self._tab_about(), "À propos") layout.addWidget(tabs) # Barre de boutons bar = QWidget() bar.setStyleSheet("background: #17202a;") btn_row = QHBoxLayout(bar) btn_row.setContentsMargins(12, 8, 12, 10) btn_cancel = QPushButton("Fermer sans sauvegarder") btn_cancel.setStyleSheet( "QPushButton { padding: 8px 20px; background: #2e4057; color: white;" " border-radius: 5px; border: none; }" "QPushButton:hover { background: #3d5166; }" ) btn_cancel.clicked.connect(self.reject) btn_save = QPushButton("Enregistrer et fermer") btn_save.setStyleSheet( "QPushButton { padding: 8px 20px; background: #1e8449; color: white;" " border-radius: 5px; font-weight: bold; border: none; }" "QPushButton:hover { background: #27ae60; }" ) btn_save.clicked.connect(self._save_and_close) btn_row.addStretch() btn_row.addWidget(btn_cancel) btn_row.addWidget(btn_save) layout.addWidget(bar) # ------------------------------------------------------------------ # # Onglet WireGuard # ------------------------------------------------------------------ # @staticmethod def _row(form: QFormLayout, text: str, widget, label_w: int = 148, field_h: int = 28) -> None: """Ligne de formulaire : label largeur fixe + champ hauteur uniforme. widget peut être un QWidget ou un QHBoxLayout (sans setFixedHeight).""" from PyQt6.QtWidgets import QLayout lbl = QLabel(text) lbl.setFixedWidth(label_w) lbl.setFixedHeight(field_h) lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) lbl.setStyleSheet("color: rgba(255,255,255,0.75); background: transparent;") if not isinstance(widget, QLayout): widget.setFixedHeight(field_h) form.addRow(lbl, widget) def _tab_wireguard(self) -> QWidget: from PyQt6.QtWidgets import QScrollArea, QListWidget outer = QWidget() outer.setStyleSheet(_TAB_CSS) outer_lay = QVBoxLayout(outer) outer_lay.setContentsMargins(0, 0, 0, 0) outer_lay.setSpacing(0) hdr = QLabel(" 🛡️ WireGuard — Configuration & Profils") hdr.setFixedHeight(36) hdr.setStyleSheet( "background: #154360; color: white; font-size: 13px;" " font-weight: bold; padding-left: 8px;" ) outer_lay.addWidget(hdr) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setStyleSheet( "QScrollArea { border: none; background: #1c2833; }" "QScrollBar:vertical { background: #1c2833; width: 8px; border: none; }" "QScrollBar::handle:vertical { background: #2e4057; border-radius: 4px; }" "QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }" ) content = QWidget() content.setStyleSheet("background: #1c2833;") lay = QVBoxLayout(content) lay.setContentsMargins(14, 12, 14, 12) lay.setSpacing(10) # ── Profils ────────────────────────────────────────────────────── grp_p = QGroupBox("Profils") gp = QVBoxLayout(grp_p) gp.setSpacing(6) gp.setContentsMargins(10, 14, 10, 10) self._profile_list = QListWidget() self._profile_list.setFixedHeight(72) self._profile_list.setStyleSheet( "QListWidget { background: rgba(255,255,255,0.07); color: white;" " border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; }" "QListWidget::item:selected { background: #2471a3; }" ) gp.addWidget(self._profile_list) p_btn_row = QHBoxLayout() p_btn_row.setSpacing(6) btn_save_p = QPushButton("💾 Sauvegarder sous…") btn_save_p.setFixedHeight(26) btn_save_p.clicked.connect(self._save_profile) btn_load_p = QPushButton("✅ Charger") btn_load_p.setFixedHeight(26) btn_load_p.clicked.connect(self._load_profile) btn_del_p = QPushButton("🗑️ Supprimer") btn_del_p.setFixedHeight(26) btn_del_p.setStyleSheet( "QPushButton { background: #6e2e1c; color: white; border-radius: 5px;" " padding: 0 12px; border: none; }" "QPushButton:hover { background: #922b21; }" ) btn_del_p.clicked.connect(self._delete_profile) p_btn_row.addWidget(btn_save_p) p_btn_row.addWidget(btn_load_p) p_btn_row.addWidget(btn_del_p) gp.addLayout(p_btn_row) lay.addWidget(grp_p) # ── Import / Export ────────────────────────────────────────────── io_row = QHBoxLayout() btn_import = QPushButton("📂 Importer un .conf") btn_import.setFixedHeight(26) btn_import.clicked.connect(self._import_conf) btn_export = QPushButton("💾 Exporter en .conf") btn_export.setFixedHeight(26) btn_export.clicked.connect(self._export_conf) io_row.addWidget(btn_import) io_row.addWidget(btn_export) io_row.addStretch() lay.addLayout(io_row) # ── Serveur ────────────────────────────────────────────────────── grp = QGroupBox("Serveur WireGuard") form = QFormLayout(grp) form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) self._srv_endpoint = QLineEdit() self._srv_endpoint.setPlaceholderText("vpn.exemple.com ou 1.2.3.4") self._row(form, "Adresse serveur :", self._srv_endpoint) self._srv_port = QSpinBox() self._srv_port.setRange(1, 65535) self._srv_port.setValue(51820) self._srv_port.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self._row(form, "Port UDP :", self._srv_port) self._srv_pubkey = QLineEdit() self._srv_pubkey.setPlaceholderText("Clé publique du serveur (base64)") self._row(form, "Clé publique serveur :", self._srv_pubkey) self._psk = QLineEdit() self._psk.setEchoMode(QLineEdit.EchoMode.Password) self._psk.setPlaceholderText("(vide si le serveur n'en utilise pas)") self._psk.setToolTip( "Clé pré-partagée (PresharedKey). Doit être identique des deux côtés :\n" "si le serveur en attend une et qu'elle manque ici, le tunnel monte\n" "mais aucun handshake n'aboutit." ) self._row(form, "Clé pré-partagée :", self._psk) lay.addWidget(grp) # ── Interface client ───────────────────────────────────────────── grp2 = QGroupBox("Interface client") form2 = QFormLayout(grp2) form2.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) self._iface_name = QLineEdit() self._iface_name.setPlaceholderText("wgs0") self._row(form2, "Nom interface :", self._iface_name) self._client_addr = QLineEdit() self._client_addr.setPlaceholderText("10.8.0.2/24") self._row(form2, "Adresse IP client :", self._client_addr) self._dns = QLineEdit() self._dns.setPlaceholderText("1.1.1.1") self._row(form2, "DNS :", self._dns) self._allowed_ips = QLineEdit() self._allowed_ips.setPlaceholderText("10.8.0.0/24") self._row(form2, "IPs autorisées :", self._allowed_ips) self._keepalive = QSpinBox() self._keepalive.setRange(0, 300) self._keepalive.setValue(25) self._keepalive.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self._row(form2, "Keepalive (s) :", self._keepalive) self._mtu = QSpinBox() self._mtu.setRange(0, 9000) self._mtu.setSpecialValueText("automatique") self._mtu.setToolTip( "0 = laisser WireGuard décider. Abaisser à 1280–1420 si le tunnel\n" "monte mais que les gros paquets ne passent pas." ) self._mtu.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self._row(form2, "MTU :", self._mtu) lay.addWidget(grp2) lay.addStretch() scroll.setWidget(content) outer_lay.addWidget(scroll) self._refresh_profile_list() return outer # ------------------------------------------------------------------ # # Onglet Clés # ------------------------------------------------------------------ # def _tab_keys(self) -> QWidget: w, lay = self._dark_page("🔑 Gestion des clés Curve25519", "#1a3a52") grp = QGroupBox("Paire de clés client") form = QFormLayout(grp) form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) self._priv_key = QLineEdit() self._priv_key.setEchoMode(QLineEdit.EchoMode.Password) self._priv_key.setPlaceholderText("(générer ou coller)") self._priv_key.setFixedHeight(28) show_priv = QPushButton("Afficher") show_priv.setFixedSize(70, 28) show_priv.setCheckable(True) show_priv.toggled.connect( lambda checked: self._priv_key.setEchoMode( QLineEdit.EchoMode.Normal if checked else QLineEdit.EchoMode.Password ) ) priv_row = QHBoxLayout() priv_row.setSpacing(6) priv_row.addWidget(self._priv_key) priv_row.addWidget(show_priv) self._row(form, "Clé privée :", priv_row) self._pub_key = QLineEdit() self._pub_key.setReadOnly(True) self._pub_key.setPlaceholderText("(dérivée automatiquement)") self._row(form, "Clé publique :", self._pub_key) btn_gen = QPushButton("Générer une nouvelle paire de clés") btn_gen.setFixedHeight(28) btn_gen.clicked.connect(self._generate_keys) self._row(form, "", btn_gen) lay.addWidget(grp) grp2 = QGroupBox("Aperçu config WireGuard") v2 = QVBoxLayout(grp2) self._config_preview = QTextEdit() self._config_preview.setReadOnly(True) self._config_preview.setFont(QFont("Courier", 9)) self._config_preview.setFixedHeight(130) v2.addWidget(self._config_preview) btn_row2 = QHBoxLayout() btn_preview = QPushButton("🔄 Rafraîchir") btn_preview.clicked.connect(self._refresh_preview) btn_qr = QPushButton("📱 Exporter QR Code") btn_qr.clicked.connect(self._export_qr) btn_row2.addWidget(btn_preview) btn_row2.addWidget(btn_qr) v2.addLayout(btn_row2) lay.addWidget(grp2) # Config serveur grp3 = QGroupBox("🖥️ Génération config serveur + client") v3 = QVBoxLayout(grp3) note = QLabel("Génère une paire complète prête à déployer (nouvelles clés, PSK).") note.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 11px;") v3.addWidget(note) btn_gen_srv = QPushButton("⚡ Générer config serveur + client") btn_gen_srv.clicked.connect(self._generate_server_config) v3.addWidget(btn_gen_srv) lay.addWidget(grp3) lay.addStretch() return w # ------------------------------------------------------------------ # # Onglet MFA # ------------------------------------------------------------------ # def _tab_mfa(self) -> QWidget: w, lay = self._dark_page("🔐 Authentification Multi-Facteurs (MFA / TOTP)", "#0e6655") self._mfa_enabled_cb = QCheckBox("Activer la vérification MFA avant connexion") self._mfa_enabled_cb.setStyleSheet("color: white; font-weight: bold;") lay.addWidget(self._mfa_enabled_cb) sep = QFrame() sep.setFrameShape(QFrame.Shape.HLine) sep.setStyleSheet("color: rgba(255,255,255,0.15);") lay.addWidget(sep) grp = QGroupBox("Secret TOTP") form = QFormLayout(grp) form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) self._mfa_secret = QLineEdit() self._mfa_secret.setPlaceholderText("(générer ou coller votre secret TOTP)") self._row(form, "Secret :", self._mfa_secret) btn_gen_secret = QPushButton("Générer un nouveau secret") btn_gen_secret.clicked.connect(self._generate_mfa_secret) self._row(form, "", btn_gen_secret) lay.addWidget(grp) grp2 = QGroupBox("QR Code — Scanner avec Bitwarden") v2 = QVBoxLayout(grp2) self._qr_label = QLabel("(générez un secret pour afficher le QR Code)") self._qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self._qr_label.setFixedHeight(230) v2.addWidget(self._qr_label) self._mfa_uri_label = QLabel("") self._mfa_uri_label.setWordWrap(True) self._mfa_uri_label.setStyleSheet("font-size: 9px; color: rgba(255,255,255,0.55);") v2.addWidget(self._mfa_uri_label) btn_show_qr = QPushButton("Afficher le QR Code") btn_show_qr.clicked.connect(self._show_qr) v2.addWidget(btn_show_qr) lay.addWidget(grp2) lay.addStretch() return w # ------------------------------------------------------------------ # # Onglet Test connexion # ------------------------------------------------------------------ # def _tab_test(self) -> QWidget: w, lay = self._dark_page("📡 Test de connectivité réseau", "#1a4a7a", scroll=True) # Test UDP serveur grp1 = QGroupBox("Diagnostic de la chaîne de connexion") g1 = QVBoxLayout(grp1) g1.addWidget(QLabel( "Vérifie toute la chaîne : configuration, clés, route, port UDP,\n" "montage du tunnel, handshake, trafic et DNS — et désigne l'étape\n" "qui bloque." )) btn_test = QPushButton("🚀 Lancer le test de connexion") btn_test.setStyleSheet( "QPushButton { padding: 9px; background: #1e8449; color: white;" " border-radius: 5px; font-weight: bold; border: none; }" "QPushButton:hover { background: #27ae60; }" ) btn_test.clicked.connect(self._run_test) g1.addWidget(btn_test) # QLabel auparavant : le rapport fait une dizaine d'étapes avec un # détail par étape, et un QLabel se laisse écraser par le layout — # le texte était rogné à quelques lignes illisibles. Une vue en # lecture seule porte sa propre barre de défilement : le rapport reste # entier quelle que soit la taille de la fenêtre. self._test_result = QTextEdit() self._test_result.setReadOnly(True) self._test_result.setMinimumHeight(320) self._test_result.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding ) self._test_result.setStyleSheet(_RESULT_CSS + "color: white;") g1.addWidget(self._test_result, 1) # Facteur d'étirement : l'espace vertical libre revient au rapport de # diagnostic plutôt qu'au ressort de fin de page. lay.addWidget(grp1, 1) # Test DNS leak grp2 = QGroupBox("🔍 Test de fuite DNS") g2 = QVBoxLayout(grp2) g2.addWidget(QLabel("Vérifie que le DNS passe bien par le tunnel VPN.")) btn_dns = QPushButton("🔍 Analyser le DNS") btn_dns.clicked.connect(self._run_dns_test) g2.addWidget(btn_dns) btn_dns_fix = QPushButton("🛠️ Réparer le DNS (tunnel arrêté)") btn_dns_fix.setToolTip( "Retire la configuration DNS laissée par un tunnel arrêté " "brutalement, qui empêche toute résolution de noms." ) btn_dns_fix.clicked.connect(self._repair_dns) g2.addWidget(btn_dns_fix) self._dns_result = QLabel("") self._dns_result.setWordWrap(True) self._dns_result.setStyleSheet( "color: white; padding: 8px; border-radius: 6px; font-size: 11px;" ) g2.addWidget(self._dns_result) lay.addWidget(grp2) # Droits d'exécution grp3 = QGroupBox("🔐 Droits d'exécution") g3 = QVBoxLayout(grp3) g3.addWidget(QLabel( "Sans règle sudo, chaque connexion ouvre un dialogue d'authentification." )) btn_priv = QPushButton("🔐 Vérifier les droits") btn_priv.clicked.connect(self._check_privileges) g3.addWidget(btn_priv) self._priv_result = QLabel("") self._priv_result.setWordWrap(True) self._priv_result.setStyleSheet( "color: white; padding: 8px; border-radius: 6px; font-size: 11px;" ) g3.addWidget(self._priv_result) lay.addWidget(grp3) return w def _refresh_profile_list(self): self._profile_list.clear() active = self._cfg.active_profile for name in self._cfg.list_profiles(): marker = "★" if name == active else " " self._profile_list.addItem(f"{marker} {name}") def _save_profile(self): from PyQt6.QtWidgets import QInputDialog self._save_values() name, ok = QInputDialog.getText(self, "Sauvegarder le profil", "Nom du profil :") if not ok or not name.strip(): return self._cfg.save_profile(name.strip()) self._refresh_profile_list() QMessageBox.information(self, "Profil sauvegardé", f"Profil « {name.strip()} » sauvegardé.") def _load_profile(self): item = self._profile_list.currentItem() if not item: return name = item.text().lstrip("★ ").strip() if self._cfg.load_profile(name): self._load_values() self._refresh_profile_list() QMessageBox.information(self, "Profil chargé", f"Profil « {name} » chargé.") def _delete_profile(self): item = self._profile_list.currentItem() if not item: return name = item.text().lstrip("★ ").strip() reply = QMessageBox.question( self, "Confirmer", f"Supprimer le profil « {name} » ?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: self._cfg.delete_profile(name) self._refresh_profile_list() # ------------------------------------------------------------------ # # Onglet Paramètres # ------------------------------------------------------------------ # def _tab_settings(self) -> QWidget: w, lay = self._dark_page("⚙️ Paramètres de l'application", "#2a1a4a") grp1 = QGroupBox("Comportement") form1 = QFormLayout(grp1) self._chk_tray = QCheckBox("Réduire dans le systray à la fermeture") self._chk_tray.setStyleSheet("color: white;") self._chk_tray.setChecked(bool(self._cfg.get("ui", "minimize_to_tray"))) form1.addRow("", self._chk_tray) self._chk_autostart = QCheckBox("Lancer WGSecure au démarrage du système") self._chk_autostart.setStyleSheet("color: white;") self._chk_autostart.setChecked(bool(self._cfg.get("ui", "autostart"))) form1.addRow("", self._chk_autostart) self._chk_auto_connect = QCheckBox( "Se connecter automatiquement au démarrage (profil actif)" ) self._chk_auto_connect.setStyleSheet("color: white;") self._chk_auto_connect.setChecked( bool(self._cfg.get("ui", "auto_connect_on_startup")) ) form1.addRow("", self._chk_auto_connect) lay.addWidget(grp1) grp2 = QGroupBox("Auto-reconnexion") form2 = QFormLayout(grp2) self._chk_autorecon = QCheckBox("Reconnecter automatiquement si le tunnel tombe") self._chk_autorecon.setStyleSheet("color: white;") self._chk_autorecon.setChecked(bool(self._cfg.get("ui", "auto_reconnect"))) form2.addRow("", self._chk_autorecon) self._spin_interval = QSpinBox() self._spin_interval.setRange(10, 300) self._spin_interval.setValue(int(self._cfg.get("ui", "reconnect_interval") or 30)) self._spin_interval.setSuffix(" s") self._spin_interval.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self._row(form2, "Intervalle :", self._spin_interval) lay.addWidget(grp2) btn_apply = QPushButton("✅ Appliquer les paramètres") btn_apply.clicked.connect(self._apply_settings) lay.addWidget(btn_apply) lay.addStretch() return w def _apply_settings(self): self._cfg.set("ui", "minimize_to_tray", self._chk_tray.isChecked()) self._cfg.set("ui", "auto_reconnect", self._chk_autorecon.isChecked()) self._cfg.set("ui", "reconnect_interval", self._spin_interval.value()) self._cfg.set("ui", "auto_connect_on_startup", self._chk_auto_connect.isChecked()) autostart = self._chk_autostart.isChecked() if autostart != bool(self._cfg.get("ui", "autostart")): self._cfg.set_autostart(autostart) else: self._cfg.save() QMessageBox.information(self, "Paramètres", "Paramètres enregistrés.") # ------------------------------------------------------------------ # # Onglet Sécurité # ------------------------------------------------------------------ # def _tab_admin(self) -> QWidget: w, lay = self._dark_page("🔒 Sécurité — Accès Administrateur", "#6e2e1c") note = QLabel( "Définissez un mot de passe pour protéger l'accès au panneau Administrateur.\n" "Laissez vide pour désactiver la protection." ) note.setWordWrap(True) note.setStyleSheet("color: rgba(255,255,255,0.8); font-size: 12px;") lay.addWidget(note) grp = QGroupBox("Mot de passe administrateur") form = QFormLayout(grp) form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) self._admin_pw1 = QLineEdit() self._admin_pw1.setEchoMode(QLineEdit.EchoMode.Password) self._admin_pw1.setPlaceholderText("Nouveau mot de passe") self._row(form, "Mot de passe :", self._admin_pw1) self._admin_pw2 = QLineEdit() self._admin_pw2.setEchoMode(QLineEdit.EchoMode.Password) self._admin_pw2.setPlaceholderText("Confirmer") self._row(form, "Confirmation :", self._admin_pw2) btn_set_pw = QPushButton("Définir le mot de passe") btn_set_pw.setStyleSheet( "QPushButton { background: #922b21; color: white;" " border-radius: 5px; border: none; }" "QPushButton:hover { background: #c0392b; }" ) btn_set_pw.clicked.connect(self._set_admin_password) self._row(form, "", btn_set_pw) lay.addWidget(grp) lay.addStretch() return w # ------------------------------------------------------------------ # # Onglet À propos # ------------------------------------------------------------------ # def _tab_about(self) -> QWidget: w = QWidget() w.setStyleSheet(_TAB_CSS) w.setAutoFillBackground(True) layout = QVBoxLayout(w) layout.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.setSpacing(12) layout.setContentsMargins(30, 24, 30, 24) from app.ui.icons import icon_app logo_lbl = QLabel() logo_lbl.setPixmap(icon_app().pixmap(86, 86)) logo_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(logo_lbl) name_lbl = QLabel("WGSecure") name_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) f = QFont(); f.setPointSize(22); f.setBold(True) name_lbl.setFont(f) name_lbl.setStyleSheet("color: white;") layout.addWidget(name_lbl) version_lbl = QLabel("Version 0.4.6") version_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) version_lbl.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 13px;") layout.addWidget(version_lbl) sep = QFrame(); sep.setFrameShape(QFrame.Shape.HLine) sep.setStyleSheet("color: rgba(255,255,255,0.15);") layout.addWidget(sep) desc_lbl = QLabel( "Interface graphique WireGuard avec surcouche MFA TOTP.\n" "Compatible Linux et Windows." ) desc_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) desc_lbl.setWordWrap(True) desc_lbl.setStyleSheet("color: white; font-size: 13px;") layout.addWidget(desc_lbl) sep2 = QFrame(); sep2.setFrameShape(QFrame.Shape.HLine) sep2.setStyleSheet("color: rgba(255,255,255,0.15);") layout.addWidget(sep2) author_lbl = QLabel("Développé par") author_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) author_lbl.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 12px;") layout.addWidget(author_lbl) brand_lbl = QLabel("JT-Tools by Johnny") brand_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) fb = QFont(); fb.setPointSize(15); fb.setBold(True) brand_lbl.setFont(fb) brand_lbl.setStyleSheet("color: #5dade2;") layout.addWidget(brand_lbl) date_lbl = QLabel("Juin 2026") date_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) date_lbl.setStyleSheet("color: rgba(255,255,255,0.55); font-size: 12px;") layout.addWidget(date_lbl) layout.addStretch() return w # ------------------------------------------------------------------ # # Chargement / sauvegarde # ------------------------------------------------------------------ # def _load_values(self): wg = self._cfg.wg self._srv_endpoint.setText(wg.get("server_endpoint", "")) self._srv_port.setValue(int(wg.get("server_port", 51820))) self._srv_pubkey.setText(wg.get("server_public_key", "")) self._psk.setText(wg.get("preshared_key", "")) self._iface_name.setText(wg.get("interface_name", "wgs0")) self._client_addr.setText(wg.get("client_address", "10.8.0.2/24")) self._dns.setText(wg.get("dns", "1.1.1.1")) self._allowed_ips.setText(wg.get("allowed_ips", "0.0.0.0/0")) self._keepalive.setValue(int(wg.get("keepalive", 25))) self._mtu.setValue(int(wg.get("mtu") or 0)) self._priv_key.setText(wg.get("client_private_key", "")) self._pub_key.setText(wg.get("client_public_key", "")) self._mfa_enabled_cb.setChecked(self._cfg.mfa_enabled) self._mfa_secret.setText(self._cfg.mfa_secret) def _save_values(self): self._cfg.set("wg", "server_endpoint", self._srv_endpoint.text().strip()) self._cfg.set("wg", "server_port", self._srv_port.value()) self._cfg.set("wg", "server_public_key", self._srv_pubkey.text().strip()) self._cfg.set("wg", "preshared_key", self._psk.text().strip()) self._cfg.set("wg", "interface_name", self._iface_name.text().strip() or "wgs0") self._cfg.set("wg", "client_address", self._client_addr.text().strip()) self._cfg.set("wg", "dns", self._dns.text().strip()) self._cfg.set("wg", "allowed_ips", self._allowed_ips.text().strip()) self._cfg.set("wg", "keepalive", self._keepalive.value()) self._cfg.set("wg", "mtu", self._mtu.value()) self._cfg.set("wg", "client_private_key", self._priv_key.text().strip()) self._cfg.set("wg", "client_public_key", self._pub_key.text().strip()) self._cfg.mfa_enabled = self._mfa_enabled_cb.isChecked() self._cfg.mfa_secret = self._mfa_secret.text().strip() self._cfg.save() def _save_and_close(self): self._save_values() QMessageBox.information(self, "Sauvegardé", "Configuration enregistrée avec succès.") self.accept() # ------------------------------------------------------------------ # # Actions # ------------------------------------------------------------------ # def _import_conf(self): from PyQt6.QtWidgets import QInputDialog import os as _os path, _ = QFileDialog.getOpenFileName( self, "Importer un fichier WireGuard", "", "WireGuard Config (*.conf);;Tous les fichiers (*)" ) if not path: return values = wg_core.parse_conf_file(path) if not values: QMessageBox.warning(self, "Erreur", "Impossible de lire ce fichier .conf") return # Demander le nom du profil (défaut = nom du fichier sans extension) default_name = _os.path.splitext(_os.path.basename(path))[0] name, ok = QInputDialog.getText( self, "Nom du profil", "Nom du nouveau profil :", text=default_name, ) if not ok or not name.strip(): return name = name.strip() # Appliquer les valeurs dans les champs mapping = { "server_endpoint": self._srv_endpoint, "server_public_key": self._srv_pubkey, "client_address": self._client_addr, "dns": self._dns, "allowed_ips": self._allowed_ips, "client_private_key": self._priv_key, "client_public_key": self._pub_key, "preshared_key": self._psk, } for k, widget in mapping.items(): if k in values: widget.setText(str(values[k])) if "server_port" in values: self._srv_port.setValue(int(values["server_port"])) if "keepalive" in values: self._keepalive.setValue(int(values["keepalive"])) if "mtu" in values: self._mtu.setValue(int(values["mtu"])) # Sauvegarder en tant que nouveau profil self._save_values() self._cfg.save_profile(name) self._refresh_profile_list() QMessageBox.information( self, "Import réussi", f"Profil « {name} » créé depuis :\n{path}" ) def _export_conf(self): self._save_values() path, _ = QFileDialog.getSaveFileName( self, "Exporter la configuration WireGuard", f"{self._cfg.wg.get('interface_name','wgs0')}.conf", "WireGuard Config (*.conf)" ) if not path: return ok, result = wg_core.export_conf_file(self._cfg, path) if ok: QMessageBox.information(self, "Export réussi", f"Config exportée :\n{result}") else: QMessageBox.warning(self, "Erreur", f"Export échoué :\n{result}") def _generate_keys(self): priv, pub = wg_core.generate_keypair() self._priv_key.setText(priv) self._pub_key.setText(pub) self._refresh_preview() QMessageBox.information( self, "Clés générées", "Nouvelle paire de clés générée.\n\n" "⚠ Copiez la clé publique sur votre serveur WireGuard." ) def _refresh_preview(self): self._save_values() self._config_preview.setPlainText(wg_core.build_client_config(self._cfg)) def _generate_mfa_secret(self): secret = mfa_core.generate_secret() self._mfa_secret.setText(secret) self._show_qr() def _show_qr(self): secret = self._mfa_secret.text().strip() if not secret: QMessageBox.warning(self, "Erreur", "Aucun secret MFA renseigné.") return pixmap = mfa_core.generate_qr_pixmap(secret) self._qr_label.setPixmap( pixmap.scaled(210, 210, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) ) self._mfa_uri_label.setText(mfa_core.get_provisioning_uri(secret)) #: Rendu d'une étape de diagnostic : puce, couleur du libellé. _STEP_STYLE = { "ok": ("✅", "#a9dfbf"), "warn": ("⚠️", "#f7dc6f"), "fail": ("❌", "#f1948a"), "skip": ("⏭️", "rgba(255,255,255,0.45)"), } def _run_test(self): from PyQt6.QtWidgets import QApplication self._save_values() self._test_result.setHtml( 'Diagnostic en cours…' ) self._test_result.setStyleSheet(_RESULT_CSS + "color: rgba(255,255,255,0.6);") QApplication.processEvents() steps = wg_core.diagnose_connection(self._cfg) self._test_result.setHtml(self._format_diagnostic(steps)) states = [s["state"] for s in steps] if "fail" in states: css = "background: rgba(231,76,60,0.20); color: #f1948a;" elif "warn" in states or "skip" in states: css = "background: rgba(241,196,15,0.15); color: #f7dc6f;" else: css = "background: rgba(39,174,96,0.20); color: #a9dfbf;" self._test_result.setStyleSheet(_RESULT_CSS + "QTextEdit { " + css + " }") def _format_diagnostic(self, steps: list[dict]) -> str: """Met en forme les étapes ; la première en échec porte le verdict.""" from html import escape rows = [] for st in steps: bullet, color = self._STEP_STYLE.get(st["state"], ("•", "white")) weight = "bold" if st["state"] == "fail" else "normal" detail = escape(st["detail"]) rows.append( f'