Files
WGSecure/app/ui/admin_window.py
T
2026-06-01 22:31:22 +02:00

536 lines
21 KiB
Python

from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
QLabel, QLineEdit, QPushButton, QSpinBox, QCheckBox,
QGroupBox, QFormLayout, QTextEdit, QMessageBox, QFrame,
)
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, 560)
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_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
# ------------------------------------------------------------------ #
def _tab_wireguard(self) -> QWidget:
w, lay = self._dark_page("Configuration WireGuard", "#154360")
grp = QGroupBox("Serveur WireGuard")
form = QFormLayout(grp)
self._srv_endpoint = QLineEdit()
self._srv_endpoint.setPlaceholderText("vpn.exemple.com ou 1.2.3.4")
form.addRow("Adresse serveur :", self._srv_endpoint)
self._srv_port = QSpinBox()
self._srv_port.setRange(1, 65535)
self._srv_port.setValue(51820)
form.addRow("Port UDP :", self._srv_port)
self._srv_pubkey = QLineEdit()
self._srv_pubkey.setPlaceholderText("Clé publique du serveur (base64)")
form.addRow("Clé publique serveur :", self._srv_pubkey)
lay.addWidget(grp)
grp2 = QGroupBox("Interface client")
form2 = QFormLayout(grp2)
self._iface_name = QLineEdit()
self._iface_name.setPlaceholderText("wgs0")
form2.addRow("Nom interface :", self._iface_name)
self._client_addr = QLineEdit()
self._client_addr.setPlaceholderText("10.8.0.2/24")
form2.addRow("Adresse IP client :", self._client_addr)
self._dns = QLineEdit()
self._dns.setPlaceholderText("1.1.1.1")
form2.addRow("DNS :", self._dns)
self._allowed_ips = QLineEdit()
self._allowed_ips.setPlaceholderText("0.0.0.0/0")
form2.addRow("IPs autorisées :", self._allowed_ips)
self._keepalive = QSpinBox()
self._keepalive.setRange(0, 300)
self._keepalive.setValue(25)
form2.addRow("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)
self._priv_key = QLineEdit()
self._priv_key.setEchoMode(QLineEdit.EchoMode.Password)
self._priv_key.setPlaceholderText("(générer ou coller)")
show_priv = QPushButton("Afficher")
show_priv.setFixedWidth(80)
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.addWidget(self._priv_key)
priv_row.addWidget(show_priv)
form.addRow("Clé privée :", priv_row)
self._pub_key = QLineEdit()
self._pub_key.setReadOnly(True)
self._pub_key.setPlaceholderText("(dérivée automatiquement)")
form.addRow("Clé publique :", self._pub_key)
btn_gen = QPushButton("Générer une nouvelle paire de clés")
btn_gen.clicked.connect(self._generate_keys)
form.addRow("", 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(150)
v2.addWidget(self._config_preview)
btn_preview = QPushButton("Rafraîchir l'aperçu")
btn_preview.clicked.connect(self._refresh_preview)
v2.addWidget(btn_preview)
lay.addWidget(grp2)
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)
self._mfa_secret = QLineEdit()
self._mfa_secret.setPlaceholderText("(générer ou coller votre secret TOTP)")
form.addRow("Secret :", self._mfa_secret)
btn_gen_secret = QPushButton("Générer un nouveau secret")
btn_gen_secret.clicked.connect(self._generate_mfa_secret)
form.addRow("", btn_gen_secret)
lay.addWidget(grp)
grp2 = QGroupBox("QR Code — Scanner avec Google Authenticator / Aegis")
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(180)
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")
info = QLabel(
"Teste la joignabilité du serveur WireGuard (port UDP) avant d'établir le tunnel."
)
info.setWordWrap(True)
info.setStyleSheet("color: white; font-size: 13px;")
lay.addWidget(info)
btn_test = QPushButton("Lancer le test de connexion")
btn_test.setStyleSheet(
"QPushButton { padding: 10px; background: #1e8449; color: white;"
" border-radius: 5px; font-weight: bold; border: none; }"
"QPushButton:hover { background: #27ae60; }"
)
btn_test.clicked.connect(self._run_test)
lay.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: 12px; border-radius: 6px; font-size: 13px;"
)
lay.addWidget(self._test_result)
lay.addStretch()
return w
# ------------------------------------------------------------------ #
# 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)
self._admin_pw1 = QLineEdit()
self._admin_pw1.setEchoMode(QLineEdit.EchoMode.Password)
self._admin_pw1.setPlaceholderText("Nouveau mot de passe")
form.addRow("Mot de passe :", self._admin_pw1)
self._admin_pw2 = QLineEdit()
self._admin_pw2.setEchoMode(QLineEdit.EchoMode.Password)
self._admin_pw2.setPlaceholderText("Confirmer")
form.addRow("Confirmation :", self._admin_pw2)
btn_set_pw = QPushButton("Définir le mot de passe")
btn_set_pw.setStyleSheet(
"QPushButton { padding: 8px; background: #922b21; color: white;"
" border-radius: 5px; border: none; }"
"QPushButton:hover { background: #c0392b; }"
)
btn_set_pw.clicked.connect(self._set_admin_password)
form.addRow("", 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.1.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 _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(160, 160, 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 _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)