Files
WGSecure/app/ui/admin_window.py
T
tuxgyver f6c3622e1d fix: handshake bloqué par la passerelle dans les IPs autorisées
Quand la passerelle qui mène au serveur appartient à une plage des « IPs
autorisées », wg-quick pose sa route vers le tunnel avec une métrique de 0.
Elle bat la route de lien de la carte réseau : la passerelle n'est plus
joignable que par le tunnel, alors que le tunnel a besoin d'elle pour sortir.
Le tunnel montait, l'interface existait, et plus aucun paquet de handshake ne
quittait la machine.

Le diagnostic détecte désormais ce cas via _next_hop() (ip route get sous
Linux, Find-NetRoute sous Windows) et le classe en échec, au lieu du simple
avertissement sur l'imprimante et le NAS qui n'en disait pas la gravité.

Corrige aussi le rapport de test de connexion : un QLabel que la mise en page
comprimait à quelques lignes tronquées, remplacé par une zone de texte
défilante ; la fenêtre d'administration, figée à 700x600, devient
redimensionnable.
2026-09-01 13:35:56 +02:00

1212 lines
50 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 à 12801420 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(
'<span style="color:rgba(255,255,255,0.6);">Diagnostic en cours…</span>'
)
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'<div style="margin-bottom:5px;">'
f'<span style="color:{color}; font-weight:{weight};">'
f'{bullet}&nbsp;{escape(st["label"])}</span>'
f'<br><span style="color:rgba(255,255,255,0.65); font-size:11px;">'
f'&nbsp;&nbsp;&nbsp;&nbsp;{detail}</span></div>'
)
failed = next((s for s in steps if s["state"] == "fail"), None)
if failed:
verdict = (f'<div style="margin-bottom:9px; font-weight:bold;">'
f'⛔ Blocage : {escape(failed["label"])}</div>')
elif any(s["state"] == "skip" for s in steps):
verdict = ('<div style="margin-bottom:9px; font-weight:bold;">'
'➡️ Chaîne validée jusqu\'au tunnel — reste à le monter'
'</div>')
else:
verdict = ('<div style="margin-bottom:9px; font-weight:bold;">'
'✅ Chaîne de connexion complète</div>')
return verdict + "".join(rows)
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 <SERVER_IP> 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)
# Sans ce bouton, la PSK générée n'existait que dans ce texte : elle
# n'était reportée nulle part, et le tunnel monté avec les clés
# affichées ne négociait jamais avec le serveur.
btn_apply = QPushButton("📥 Appliquer le volet client à cette configuration")
btn_apply.setStyleSheet(
"QPushButton { background:#1e8449; color:white; border-radius:5px;"
" padding:7px 16px; border:none; } QPushButton:hover { background:#27ae60; }"
)
def _apply():
self._priv_key.setText(result["client_priv"])
self._pub_key.setText(result["client_pub"])
self._srv_pubkey.setText(result["server_pub"])
self._psk.setText(result["psk"])
self._refresh_preview()
dlg.accept()
QMessageBox.information(
self, "Config client appliquée",
"Clés client, clé publique du serveur et clé pré-partagée "
"reportées dans la configuration.\n\n"
"⚠ Déployez le volet serveur sur la machine distante et "
"renseignez son adresse publique."
)
btn_apply.clicked.connect(_apply)
v.addWidget(btn_apply)
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;"
elif status == "down":
lines.append("🚫 Résolution DNS hors service — utilisez « Réparer le DNS »")
css = "background: rgba(231,76,60,0.2); color: #f1948a;"
elif not r.get("tunnel_up", False):
lines.append("️ Tunnel inactif : la comparaison n'a pas de sens")
css = "color: #aed6f1;"
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 _check_privileges(self):
"""Liste les commandes privilégiées et celles qui exigeront un dialogue."""
from PyQt6.QtWidgets import QApplication
from app.utils.platform_utils import privilege_report
self._priv_result.setText("Vérification en cours…")
QApplication.processEvents()
rows = privilege_report()
if not rows:
self._priv_result.setText(
"✅ Exécution avec les privilèges root : rien à configurer."
)
css = "background: rgba(39,174,96,0.2); color: #a9dfbf;"
else:
lines = [
("✅ " if ok else "⚠️ ") + f"{name}{role}"
+ ("" if ok else " (dialogue à chaque appel)")
for name, ok, role in rows
]
missing = [n for n, ok, _ in rows if not ok]
if missing:
lines.append("")
lines.append("Pour supprimer ces dialogues : make setup-sudoers")
css = "background: rgba(241,196,15,0.2); color: #f9e79f;"
else:
css = "background: rgba(39,174,96,0.2); color: #a9dfbf;"
self._priv_result.setText("\n".join(lines))
self._priv_result.setStyleSheet(
f"padding: 8px; border-radius: 6px; font-size: 11px; {css}"
)
def _repair_dns(self):
"""Retire la configuration DNS résiduelle d'un tunnel mal arrêté.
`wg-quick down` refuse de s'exécuter dès que l'interface a disparu et
ne retire donc jamais l'entrée DNS qu'il avait posée : la résolution
reste dirigée vers un serveur injoignable.
"""
from PyQt6.QtWidgets import QApplication
self._dns_result.setText("Réparation en cours…")
QApplication.processEvents()
actions = wg_core.force_cleanup(self._cfg)
works = dns_util.resolution_works()
if works:
msg = "✅ Résolution DNS fonctionnelle"
if actions:
msg += "\nActions : " + ", ".join(actions)
css = "background: rgba(39,174,96,0.2); color: #a9dfbf;"
elif actions:
msg = ("⚠️ Résidus retirés (" + ", ".join(actions) +
") mais la résolution échoue toujours.\n"
"Vérifiez la connexion réseau physique.")
css = "background: rgba(241,196,15,0.2); color: #f9e79f;"
else:
msg = ("🚫 Aucun résidu du tunnel détecté et la résolution échoue :"
" le problème vient du réseau, pas de WGSecure.")
css = "background: rgba(231,76,60,0.2); color: #f1948a;"
self._dns_result.setText(msg)
self._dns_result.setStyleSheet(
f"padding: 8px; border-radius: 6px; font-size: 11px; {css}"
)
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)