1073 lines
44 KiB
Python
1073 lines
44 KiB
Python
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; }
|
||
"""
|
||
|
||
|
||
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_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)
|
||
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)
|
||
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")
|
||
|
||
# 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)
|
||
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)
|
||
|
||
lay.addStretch()
|
||
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.1")
|
||
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 <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)
|
||
|
||
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)
|