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 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; } """ 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 ) self.setFixedSize(700, 600) self._build_ui() self._load_values() # ------------------------------------------------------------------ # # Helper : page sombre avec bandeau coloré # ------------------------------------------------------------------ # def _dark_page(self, banner_text: str, banner_color: str): """Retourne (page_widget, body_layout) : fond sombre + bandeau.""" 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) 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_profiles(), "Profils") 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: w, lay = self._dark_page("🛡️ Configuration WireGuard", "#154360") # Barre import / export io_row = QHBoxLayout() btn_import = QPushButton("📂 Importer un .conf") btn_import.clicked.connect(self._import_conf) btn_export = QPushButton("💾 Exporter en .conf") btn_export.clicked.connect(self._export_conf) io_row.addWidget(btn_import) io_row.addWidget(btn_export) io_row.addStretch() lay.addLayout(io_row) 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) lay.addWidget(grp) 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("0.0.0.0/0") 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) lay.addWidget(grp2) lay.addStretch() return w # ------------------------------------------------------------------ # # 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") # Test UDP serveur grp1 = QGroupBox("Test UDP serveur") g1 = QVBoxLayout(grp1) g1.addWidget(QLabel("Vérifie la joignabilité UDP du serveur WireGuard.")) 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) self._test_result = QLabel("") self._test_result.setWordWrap(True) self._test_result.setAlignment(Qt.AlignmentFlag.AlignCenter) self._test_result.setStyleSheet( "color: white; padding: 8px; border-radius: 6px; font-size: 12px;" ) g1.addWidget(self._test_result) lay.addWidget(grp1) # 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) 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) lay.addStretch() return w # ------------------------------------------------------------------ # # Onglet Profils # ------------------------------------------------------------------ # def _tab_profiles(self) -> QWidget: from PyQt6.QtWidgets import QListWidget, QListWidgetItem, QInputDialog w, lay = self._dark_page("👤 Gestion des profils", "#1a4a2a") info = QLabel( "Sauvegardez la configuration WireGuard courante sous un nom de profil,\n" "puis basculez entre profils sans passer par la configuration." ) info.setWordWrap(True) info.setStyleSheet("color: rgba(255,255,255,0.75); font-size: 12px;") lay.addWidget(info) grp = QGroupBox("Profils sauvegardés") gv = QVBoxLayout(grp) self._profile_list = QListWidget() self._profile_list.setFixedHeight(130) 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; }" ) gv.addWidget(self._profile_list) btn_row = QHBoxLayout() btn_save_p = QPushButton("💾 Sauvegarder sous…") btn_save_p.clicked.connect(self._save_profile) btn_load_p = QPushButton("✅ Charger") btn_load_p.clicked.connect(self._load_profile) btn_del_p = QPushButton("🗑️ Supprimer") btn_del_p.setStyleSheet( "QPushButton { background: #6e2e1c; color: white; border-radius: 5px;" " padding: 7px 12px; border: none; }" "QPushButton:hover { background: #922b21; }" ) btn_del_p.clicked.connect(self._delete_profile) btn_row.addWidget(btn_save_p) btn_row.addWidget(btn_load_p) btn_row.addWidget(btn_del_p) gv.addLayout(btn_row) lay.addWidget(grp) lay.addStretch() self._refresh_profile_list() 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) grp_ks = QGroupBox("🔒 Kill Switch (Linux)") g_ks = QVBoxLayout(grp_ks) note_ks = QLabel( "Bloque tout trafic internet si le tunnel VPN tombe.\n" "Nécessite des droits root (iptables)." ) note_ks.setWordWrap(True) note_ks.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 11px;") g_ks.addWidget(note_ks) ks_row = QHBoxLayout() self._btn_ks_enable = QPushButton("🔒 Activer") self._btn_ks_enable.clicked.connect(self._enable_kill_switch) self._btn_ks_disable = QPushButton("🔓 Désactiver") self._btn_ks_disable.setStyleSheet( "QPushButton { background: #6e2e1c; border-radius: 5px; padding: 7px 12px;" " color: white; border: none; } QPushButton:hover { background: #922b21; }" ) self._btn_ks_disable.clicked.connect(self._disable_kill_switch) self._ks_status = QLabel("—") self._ks_status.setStyleSheet("font-size: 11px; color: #aed6f1;") ks_row.addWidget(self._btn_ks_enable) ks_row.addWidget(self._btn_ks_disable) ks_row.addWidget(self._ks_status) ks_row.addStretch() g_ks.addLayout(ks_row) lay.addWidget(grp_ks) self._refresh_ks_status() 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.3.0") 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._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._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", "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", "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, } 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"])) # 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)) def _run_test(self): self._save_values() self._test_result.setText("Test en cours…") self._test_result.setStyleSheet( "color: rgba(255,255,255,0.6); padding: 12px; border-radius: 6px; font-size: 13px;" ) from PyQt6.QtWidgets import QApplication QApplication.processEvents() ok, msg = wg_core.test_connection(self._cfg) if ok: self._test_result.setText(f"✓ {msg}") self._test_result.setStyleSheet( "padding: 12px; border-radius: 6px; font-size: 13px; font-weight: bold;" "background: rgba(39,174,96,0.25); color: #a9dfbf;" ) else: self._test_result.setText(f"✗ {msg}") self._test_result.setStyleSheet( "padding: 12px; border-radius: 6px; font-size: 13px; font-weight: bold;" "background: rgba(231,76,60,0.25); color: #f1948a;" ) def _export_qr(self): """Exporte la config client comme QR Code pour import mobile.""" self._save_values() if not self._cfg.configured: QMessageBox.warning(self, "Erreur", "Config WireGuard incomplète.") return from PyQt6.QtWidgets import QDialog, QLabel, QVBoxLayout import qrcode, io from PyQt6.QtGui import QImage, QPixmap content = wg_core.build_client_config(self._cfg) qr = qrcode.QRCode(box_size=5, border=2, error_correction=qrcode.constants.ERROR_CORRECT_L) qr.add_data(content) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") buf = io.BytesIO() img.save(buf, "PNG") buf.seek(0) pix = QPixmap.fromImage(QImage.fromData(buf.read())) dlg = QDialog(self) dlg.setWindowTitle("QR Code — Config WireGuard client") dlg.setStyleSheet("background: #1c2833; color: white;") v = QVBoxLayout(dlg) lbl = QLabel() lbl.setPixmap(pix.scaled(300, 300, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) note = QLabel("Scannez avec l'application WireGuard sur mobile.") note.setAlignment(Qt.AlignmentFlag.AlignCenter) note.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 11px;") v.addWidget(lbl) v.addWidget(note) dlg.exec() def _generate_server_config(self): """Génère et affiche une paire complète server + client.""" from PyQt6.QtWidgets import QDialog, QVBoxLayout, QTabWidget, QTextEdit, QPushButton result = wg_core.generate_server_config( server_port=self._srv_port.value() if hasattr(self, '_srv_port') else 51820 ) dlg = QDialog(self) dlg.setWindowTitle("Config générée — Server + Client") dlg.setMinimumSize(600, 420) dlg.setStyleSheet("background: #1c2833; color: white;") v = QVBoxLayout(dlg) tabs = QTabWidget() tabs.setStyleSheet( "QTabBar::tab { background:#2e4057; color:white; padding:6px 14px; }" "QTabBar::tab:selected { background:#2471a3; }" ) for title, content in [("🖥️ Serveur", result["server_conf"]), ("💻 Client", result["client_conf"])]: te = QTextEdit() te.setReadOnly(True) te.setFont(QFont("Courier", 9)) te.setPlainText(content) te.setStyleSheet( "background: #17202a; color: #a8d8ea; border: none; font-size: 11px;" ) tabs.addTab(te, title) v.addWidget(tabs) note = QLabel( f"🔑 Clé pub. serveur : {result['server_pub'][:32]}…\n" f"🔑 Clé pub. client : {result['client_pub'][:32]}…\n" "⚠️ Remplacez dans la config client par l'IP publique du serveur." ) note.setWordWrap(True) note.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 10px; padding: 6px;") v.addWidget(note) btn_close = QPushButton("Fermer") btn_close.setStyleSheet( "QPushButton { background:#2471a3; color:white; border-radius:5px;" " padding:7px 16px; border:none; } QPushButton:hover { background:#1a5276; }" ) btn_close.clicked.connect(dlg.accept) v.addWidget(btn_close) dlg.exec() def _run_dns_test(self): from PyQt6.QtWidgets import QApplication self._dns_result.setText("Analyse en cours…") QApplication.processEvents() r = wg_core.dns_leak_test(self._cfg) lines = [] status = r["status"] if status == "ok": lines.append("✅ Aucune fuite DNS détectée") css = "background: rgba(39,174,96,0.2); color: #a9dfbf;" elif status == "leak": lines.append("⚠️ Fuite DNS potentielle !") css = "background: rgba(231,76,60,0.2); color: #f1948a;" else: lines.append("❓ Statut inconnu") css = "color: #aed6f1;" if r["resolvers"]: lines.append("Serveurs DNS actifs : " + " • ".join(r["resolvers"])) if r["expected"]: lines.append(f"DNS configuré (VPN) : {r['expected']}") if r.get("probe_ip"): lines.append(f"IP retournée par whoami.akamai.net : {r['probe_ip']}") self._dns_result.setText("\n".join(lines)) self._dns_result.setStyleSheet( f"padding: 8px; border-radius: 6px; font-size: 11px; {css}" ) def _refresh_ks_status(self): active = wg_core.kill_switch_active() if active: self._ks_status.setText("🔒 Actif") self._ks_status.setStyleSheet("color: #a9dfbf; font-size: 11px;") else: self._ks_status.setText("🔓 Inactif") self._ks_status.setStyleSheet("color: #f1948a; font-size: 11px;") def _enable_kill_switch(self): self._save_values() ok, msg = wg_core.enable_kill_switch(self._cfg) self._refresh_ks_status() if ok: QMessageBox.information(self, "Kill Switch", msg) else: QMessageBox.warning(self, "Kill Switch", msg) def _disable_kill_switch(self): ok, msg = wg_core.disable_kill_switch(self._cfg) self._refresh_ks_status() QMessageBox.information(self, "Kill Switch", msg) def _set_admin_password(self): p1 = self._admin_pw1.text() p2 = self._admin_pw2.text() if p1 != p2: QMessageBox.warning(self, "Erreur", "Les mots de passe ne correspondent pas.") return self._cfg.set_admin_password(p1) self._admin_pw1.clear() self._admin_pw2.clear() msg = "Mot de passe supprimé." if not p1 else "Mot de passe administrateur mis à jour." QMessageBox.information(self, "Succès", msg)