Initial release
This commit is contained in:
@@ -0,0 +1,535 @@
|
||||
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)
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
"""Icônes WGSecure — bouclier VPN dessiné via QPainter."""
|
||||
from PyQt6.QtGui import (
|
||||
QIcon, QPixmap, QPainter, QColor, QBrush, QPen,
|
||||
QFont, QPainterPath, QLinearGradient,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QRect, QPointF
|
||||
|
||||
|
||||
def _shield(
|
||||
size: int,
|
||||
body_top: str,
|
||||
body_bot: str,
|
||||
dot_color: str,
|
||||
dot: bool = True,
|
||||
) -> QPixmap:
|
||||
"""Dessine un bouclier avec dégradé vertical et un indicateur coloré."""
|
||||
pix = QPixmap(size, size)
|
||||
pix.fill(Qt.GlobalColor.transparent)
|
||||
p = QPainter(pix)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
s = size
|
||||
m = s * 0.06 # marge
|
||||
|
||||
# --- Forme bouclier ---
|
||||
path = QPainterPath()
|
||||
path.moveTo(s / 2, m)
|
||||
path.lineTo(s - m, s * 0.25)
|
||||
path.cubicTo(
|
||||
s - m, s * 0.65,
|
||||
s * 0.72, s * 0.85,
|
||||
s / 2, s - m,
|
||||
)
|
||||
path.cubicTo(
|
||||
s * 0.28, s * 0.85,
|
||||
m, s * 0.65,
|
||||
m, s * 0.25,
|
||||
)
|
||||
path.closeSubpath()
|
||||
|
||||
# Dégradé vertical corps du bouclier
|
||||
grad = QLinearGradient(QPointF(s / 2, m), QPointF(s / 2, s - m))
|
||||
grad.setColorAt(0.0, QColor(body_top))
|
||||
grad.setColorAt(1.0, QColor(body_bot))
|
||||
p.setBrush(QBrush(grad))
|
||||
p.setPen(Qt.PenStyle.NoPen)
|
||||
p.drawPath(path)
|
||||
|
||||
# Liseré blanc semi-transparent
|
||||
pen = QPen(QColor(255, 255, 255, 60))
|
||||
pen.setWidthF(s * 0.04)
|
||||
p.setPen(pen)
|
||||
p.setBrush(Qt.BrushStyle.NoBrush)
|
||||
p.drawPath(path)
|
||||
|
||||
# --- "W" centré ---
|
||||
p.setPen(QPen(QColor(255, 255, 255, 230)))
|
||||
font = QFont("Arial", max(7, int(s * 0.38)), QFont.Weight.Bold)
|
||||
p.setFont(font)
|
||||
rect = QRect(int(s * 0.08), int(s * 0.22), int(s * 0.84), int(s * 0.55))
|
||||
p.drawText(rect, Qt.AlignmentFlag.AlignCenter, "W")
|
||||
|
||||
# --- Point de statut (coin bas-droit) ---
|
||||
if dot:
|
||||
dr = s * 0.22
|
||||
dx = s - m - dr * 0.5
|
||||
dy = s * 0.68
|
||||
# Halo blanc
|
||||
p.setPen(Qt.PenStyle.NoPen)
|
||||
p.setBrush(QBrush(QColor(255, 255, 255, 200)))
|
||||
p.drawEllipse(QPointF(dx, dy), dr * 0.72, dr * 0.72)
|
||||
# Dot coloré
|
||||
p.setBrush(QBrush(QColor(dot_color)))
|
||||
p.drawEllipse(QPointF(dx, dy), dr * 0.55, dr * 0.55)
|
||||
|
||||
p.end()
|
||||
return pix
|
||||
|
||||
|
||||
def icon_connected(size: int = 32) -> QIcon:
|
||||
return QIcon(_shield(size, "#1a7a4a", "#0d5c35", "#2ecc71", dot=True))
|
||||
|
||||
|
||||
def icon_disconnected(size: int = 32) -> QIcon:
|
||||
return QIcon(_shield(size, "#2c3e50", "#1a252f", "#e74c3c", dot=True))
|
||||
|
||||
|
||||
def icon_connecting(size: int = 32) -> QIcon:
|
||||
return QIcon(_shield(size, "#7d5a00", "#4a3500", "#f39c12", dot=True))
|
||||
|
||||
|
||||
def icon_app(size: int = 32) -> QIcon:
|
||||
"""Icône principale multi-tailles — bouclier bleu sans point de statut.
|
||||
Plusieurs résolutions pour la barre des tâches, l'alt-tab et le systray."""
|
||||
ico = QIcon()
|
||||
for s in (16, 24, 32, 48, 64, 128, 256):
|
||||
ico.addPixmap(_shield(s, "#2980b9", "#1a5276", "#ffffff", dot=False))
|
||||
return ico
|
||||
|
||||
|
||||
def icon_admin(size: int = 32) -> QIcon:
|
||||
return QIcon(_shield(size, "#6c3483", "#4a235a", "#9b59b6", dot=True))
|
||||
|
||||
|
||||
def pixmap_app(size: int = 64) -> QPixmap:
|
||||
return _shield(size, "#2980b9", "#1a5276", "#ffffff", dot=False)
|
||||
@@ -0,0 +1,297 @@
|
||||
from PyQt6.QtWidgets import (
|
||||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QPushButton, QFrame, QMessageBox, QInputDialog,
|
||||
QLineEdit,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QTimer
|
||||
from PyQt6.QtGui import QFont, QCloseEvent, QPalette, QColor
|
||||
|
||||
from app.core.config import Config
|
||||
from app.core import wireguard as wg_core
|
||||
from app.core import mfa as mfa_core
|
||||
from app.ui.mfa_dialog import MFADialog
|
||||
from app.ui.admin_window import AdminWindow
|
||||
from app.ui import icons
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self, config: Config, parent=None):
|
||||
super().__init__(parent)
|
||||
self._cfg = config
|
||||
self._connecting = False
|
||||
self.setWindowTitle("WGSecure")
|
||||
self.setFixedSize(400, 520)
|
||||
self.setWindowFlags(
|
||||
Qt.WindowType.Window
|
||||
| Qt.WindowType.WindowTitleHint
|
||||
| Qt.WindowType.WindowCloseButtonHint
|
||||
| Qt.WindowType.WindowMinimizeButtonHint
|
||||
)
|
||||
self.setWindowIcon(icons.icon_app())
|
||||
self._build_ui()
|
||||
self._status_timer = QTimer(self)
|
||||
self._status_timer.timeout.connect(self._refresh_status)
|
||||
self._status_timer.start(3000)
|
||||
self._refresh_status()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Construction UI
|
||||
# ------------------------------------------------------------------ #
|
||||
def _build_ui(self):
|
||||
central = QWidget()
|
||||
self.setCentralWidget(central)
|
||||
layout = QVBoxLayout(central)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# -- En-tête --------------------------------------------------
|
||||
header = QWidget()
|
||||
header.setFixedHeight(72)
|
||||
header.setStyleSheet("background: #2c3e50;")
|
||||
h_layout = QHBoxLayout(header)
|
||||
h_layout.setContentsMargins(16, 10, 16, 10)
|
||||
|
||||
self._icon_label = QLabel()
|
||||
self._icon_label.setFixedSize(44, 44)
|
||||
self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
h_layout.addWidget(self._icon_label)
|
||||
|
||||
title_col = QVBoxLayout()
|
||||
title_col.setSpacing(2)
|
||||
app_title = QLabel("WGSecure")
|
||||
app_title.setStyleSheet("color: white; font-size: 17px; font-weight: bold; background: transparent;")
|
||||
version_label = QLabel("v0.1.0 — WireGuard + MFA")
|
||||
version_label.setStyleSheet("color: #95a5a6; font-size: 10px; background: transparent;")
|
||||
title_col.addWidget(app_title)
|
||||
title_col.addWidget(version_label)
|
||||
h_layout.addLayout(title_col)
|
||||
h_layout.addStretch()
|
||||
layout.addWidget(header)
|
||||
|
||||
# -- Zone statut ----------------------------------------------
|
||||
status_area = QWidget()
|
||||
status_area.setFixedHeight(90)
|
||||
status_area.setStyleSheet("background: #ecf0f1;")
|
||||
s_layout = QVBoxLayout(status_area)
|
||||
s_layout.setContentsMargins(10, 10, 10, 10)
|
||||
s_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self._status_badge = QLabel("● Déconnecté")
|
||||
self._status_badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self._status_badge.setStyleSheet("color: #e74c3c; font-size: 16px; font-weight: bold; background: transparent;")
|
||||
s_layout.addWidget(self._status_badge)
|
||||
|
||||
self._status_sub = QLabel("")
|
||||
self._status_sub.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self._status_sub.setStyleSheet("color: #7f8c8d; font-size: 11px; background: transparent;")
|
||||
s_layout.addWidget(self._status_sub)
|
||||
layout.addWidget(status_area)
|
||||
|
||||
# -- Séparateur -----------------------------------------------
|
||||
sep = QFrame()
|
||||
sep.setFrameShape(QFrame.Shape.HLine)
|
||||
sep.setStyleSheet("color: #bdc3c7;")
|
||||
layout.addWidget(sep)
|
||||
|
||||
# -- Section infos --------------------------------------------
|
||||
info_frame = QFrame()
|
||||
info_frame.setFrameShape(QFrame.Shape.NoFrame)
|
||||
info_frame.setStyleSheet("background-color: #ffffff;")
|
||||
info_frame.setAutoFillBackground(True)
|
||||
i_layout = QVBoxLayout(info_frame)
|
||||
i_layout.setContentsMargins(16, 12, 16, 12)
|
||||
i_layout.setSpacing(8)
|
||||
|
||||
self._info_labels: dict[str, QLabel] = {}
|
||||
rows = [
|
||||
("server", "Serveur"),
|
||||
("iface", "Interface"),
|
||||
("addr", "Adresse IP"),
|
||||
("mfa", "MFA"),
|
||||
("rx_tx", "Transfert"),
|
||||
("handshake", "Dernier handshake"),
|
||||
]
|
||||
for key, label in rows:
|
||||
row_w = QWidget()
|
||||
row_w.setStyleSheet("background: transparent;")
|
||||
row_layout = QHBoxLayout(row_w)
|
||||
row_layout.setContentsMargins(0, 0, 0, 0)
|
||||
row_layout.setSpacing(8)
|
||||
|
||||
lbl_key = QLabel(f"{label} :")
|
||||
lbl_key.setFixedWidth(130)
|
||||
lbl_key.setStyleSheet("color: #7f8c8d; font-size: 12px; background: transparent;")
|
||||
|
||||
lbl_val = QLabel("—")
|
||||
lbl_val.setStyleSheet("color: #2c3e50; font-size: 12px; background: transparent;")
|
||||
lbl_val.setWordWrap(True)
|
||||
self._info_labels[key] = lbl_val
|
||||
|
||||
row_layout.addWidget(lbl_key)
|
||||
row_layout.addWidget(lbl_val, 1)
|
||||
i_layout.addWidget(row_w)
|
||||
|
||||
layout.addWidget(info_frame, 1) # stretch factor 1 — prend l'espace disponible
|
||||
|
||||
# -- Séparateur bas -------------------------------------------
|
||||
sep2 = QFrame()
|
||||
sep2.setFrameShape(QFrame.Shape.HLine)
|
||||
sep2.setStyleSheet("color: #bdc3c7;")
|
||||
layout.addWidget(sep2)
|
||||
|
||||
# -- Boutons --------------------------------------------------
|
||||
btn_area = QWidget()
|
||||
btn_area.setStyleSheet("background: #f8f9fa;")
|
||||
b_layout = QVBoxLayout(btn_area)
|
||||
b_layout.setContentsMargins(16, 12, 16, 12)
|
||||
b_layout.setSpacing(8)
|
||||
|
||||
self._btn_connect = QPushButton("Se connecter")
|
||||
self._btn_connect.setFixedHeight(42)
|
||||
self._btn_connect.setStyleSheet(
|
||||
"QPushButton { background: #27ae60; color: white; font-size: 14px; "
|
||||
"font-weight: bold; border-radius: 6px; border: none; }"
|
||||
"QPushButton:hover { background: #219a52; }"
|
||||
"QPushButton:disabled { background: #bdc3c7; color: #ecf0f1; }"
|
||||
)
|
||||
self._btn_connect.clicked.connect(self._on_connect)
|
||||
b_layout.addWidget(self._btn_connect)
|
||||
|
||||
btn_admin = QPushButton("Panneau Administrateur")
|
||||
btn_admin.setFixedHeight(34)
|
||||
btn_admin.setStyleSheet(
|
||||
"QPushButton { background: #dde1e7; color: #2c3e50; border-radius: 6px; "
|
||||
"font-size: 12px; border: none; }"
|
||||
"QPushButton:hover { background: #bdc3c7; color: #2c3e50; }"
|
||||
)
|
||||
btn_admin.clicked.connect(self._open_admin)
|
||||
b_layout.addWidget(btn_admin)
|
||||
layout.addWidget(btn_area)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Rafraîchissement du statut
|
||||
# ------------------------------------------------------------------ #
|
||||
def _refresh_status(self):
|
||||
wg = self._cfg.wg
|
||||
srv = wg.get("server_endpoint", "")
|
||||
port = wg.get("server_port", "")
|
||||
self._info_labels["server"].setText(f"{srv}:{port}" if srv else "—")
|
||||
self._info_labels["iface"].setText(wg.get("interface_name", "wgs0"))
|
||||
self._info_labels["addr"].setText(wg.get("client_address", "—"))
|
||||
self._info_labels["mfa"].setText(
|
||||
"Activé ✓" if self._cfg.mfa_enabled else "Désactivé"
|
||||
)
|
||||
|
||||
try:
|
||||
info = wg_core.get_status_info(self._cfg)
|
||||
connected = info["connected"]
|
||||
except Exception:
|
||||
connected = False
|
||||
info = {}
|
||||
|
||||
if connected:
|
||||
self._status_badge.setText("● Connecté")
|
||||
self._status_badge.setStyleSheet(
|
||||
"color: #27ae60; font-size: 16px; font-weight: bold; background: transparent;"
|
||||
)
|
||||
self._btn_connect.setText("Se déconnecter")
|
||||
self._btn_connect.setStyleSheet(
|
||||
"QPushButton { background: #e74c3c; color: white; font-size: 14px; "
|
||||
"font-weight: bold; border-radius: 6px; border: none; }"
|
||||
"QPushButton:hover { background: #c0392b; }"
|
||||
)
|
||||
self._info_labels["rx_tx"].setText(
|
||||
f"↓ {info.get('rx_bytes','—')} ↑ {info.get('tx_bytes','—')}"
|
||||
)
|
||||
self._info_labels["handshake"].setText(info.get("last_handshake", "—"))
|
||||
self._status_sub.setText(f"Interface active : {info.get('interface','—')}")
|
||||
self.setWindowIcon(icons.icon_connected())
|
||||
else:
|
||||
self._status_badge.setText("● Déconnecté")
|
||||
self._status_badge.setStyleSheet(
|
||||
"color: #e74c3c; font-size: 16px; font-weight: bold; background: transparent;"
|
||||
)
|
||||
self._btn_connect.setText("Se connecter")
|
||||
self._btn_connect.setStyleSheet(
|
||||
"QPushButton { background: #27ae60; color: white; font-size: 14px; "
|
||||
"font-weight: bold; border-radius: 6px; border: none; }"
|
||||
"QPushButton:hover { background: #219a52; }"
|
||||
)
|
||||
self._info_labels["rx_tx"].setText("—")
|
||||
self._info_labels["handshake"].setText("—")
|
||||
self._status_sub.setText("")
|
||||
self.setWindowIcon(icons.icon_disconnected())
|
||||
|
||||
pix = (icons.icon_connected() if connected else icons.icon_disconnected()).pixmap(36, 36)
|
||||
self._icon_label.setPixmap(pix)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Actions
|
||||
# ------------------------------------------------------------------ #
|
||||
def _on_connect(self):
|
||||
if self._connecting:
|
||||
return
|
||||
try:
|
||||
connected = wg_core.is_connected(self._cfg)
|
||||
except Exception:
|
||||
connected = False
|
||||
|
||||
if connected:
|
||||
ok, msg = wg_core.disconnect(self._cfg)
|
||||
if not ok:
|
||||
QMessageBox.warning(self, "Erreur", f"Déconnexion échouée :\n{msg}")
|
||||
else:
|
||||
if not self._cfg.configured:
|
||||
QMessageBox.information(
|
||||
self, "Configuration manquante",
|
||||
"WireGuard n'est pas encore configuré.\n"
|
||||
"Ouvrez le panneau Administrateur pour paramétrer la connexion."
|
||||
)
|
||||
return
|
||||
if self._cfg.mfa_enabled:
|
||||
if not self._cfg.mfa_secret:
|
||||
QMessageBox.warning(
|
||||
self, "MFA non configuré",
|
||||
"Le MFA est activé mais aucun secret n'est configuré."
|
||||
)
|
||||
return
|
||||
dlg = MFADialog(self._cfg.mfa_secret, self)
|
||||
if dlg.exec() != MFADialog.DialogCode.Accepted or not dlg.is_verified():
|
||||
return
|
||||
|
||||
self._btn_connect.setEnabled(False)
|
||||
self._btn_connect.setText("Connexion…")
|
||||
self._connecting = True
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
QApplication.processEvents()
|
||||
|
||||
ok, msg = wg_core.connect(self._cfg)
|
||||
self._connecting = False
|
||||
self._btn_connect.setEnabled(True)
|
||||
if not ok:
|
||||
QMessageBox.warning(self, "Erreur de connexion", msg)
|
||||
|
||||
self._refresh_status()
|
||||
|
||||
def _open_admin(self):
|
||||
if self._cfg.has_admin_password():
|
||||
pw, ok = QInputDialog.getText(
|
||||
self, "Accès Administrateur",
|
||||
"Mot de passe administrateur :",
|
||||
QLineEdit.EchoMode.Password,
|
||||
)
|
||||
if not ok:
|
||||
return
|
||||
if not self._cfg.check_admin_password(pw):
|
||||
QMessageBox.warning(self, "Refusé", "Mot de passe incorrect.")
|
||||
return
|
||||
dlg = AdminWindow(self._cfg, self)
|
||||
dlg.exec()
|
||||
self._refresh_status()
|
||||
|
||||
def closeEvent(self, event: QCloseEvent):
|
||||
if self._cfg.get("ui", "minimize_to_tray"):
|
||||
event.ignore()
|
||||
self.hide()
|
||||
else:
|
||||
event.accept()
|
||||
@@ -0,0 +1,130 @@
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QProgressBar, QFrame,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QTimer
|
||||
from PyQt6.QtGui import QFont
|
||||
from app.core import mfa as mfa_core
|
||||
|
||||
|
||||
class MFADialog(QDialog):
|
||||
def __init__(self, secret: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self._secret = secret
|
||||
self._verified = False
|
||||
self.setWindowTitle("WGSecure — Authentification MFA")
|
||||
self.setFixedWidth(340)
|
||||
self.setModal(True)
|
||||
self._build_ui()
|
||||
self._timer = QTimer(self)
|
||||
self._timer.timeout.connect(self._tick)
|
||||
self._timer.start(500)
|
||||
self._tick()
|
||||
|
||||
def _build_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(12)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
|
||||
title = QLabel("Vérification en deux étapes")
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
f = QFont()
|
||||
f.setPointSize(13)
|
||||
f.setBold(True)
|
||||
title.setFont(f)
|
||||
layout.addWidget(title)
|
||||
|
||||
sub = QLabel("Entrez le code à 6 chiffres de votre application d'authentification.")
|
||||
sub.setWordWrap(True)
|
||||
sub.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
sub.setStyleSheet("color: #555;")
|
||||
layout.addWidget(sub)
|
||||
|
||||
sep = QFrame()
|
||||
sep.setFrameShape(QFrame.Shape.HLine)
|
||||
sep.setStyleSheet("color: #ddd;")
|
||||
layout.addWidget(sep)
|
||||
|
||||
self._code_input = QLineEdit()
|
||||
self._code_input.setPlaceholderText("000 000")
|
||||
self._code_input.setMaxLength(7)
|
||||
self._code_input.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
f2 = QFont("Courier", 20)
|
||||
f2.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, 4)
|
||||
self._code_input.setFont(f2)
|
||||
self._code_input.setStyleSheet(
|
||||
"padding: 8px; border: 2px solid #3498db; border-radius: 6px;"
|
||||
)
|
||||
self._code_input.returnPressed.connect(self._verify)
|
||||
layout.addWidget(self._code_input)
|
||||
|
||||
self._error_label = QLabel("")
|
||||
self._error_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self._error_label.setStyleSheet("color: #e74c3c; font-weight: bold;")
|
||||
layout.addWidget(self._error_label)
|
||||
|
||||
self._progress = QProgressBar()
|
||||
self._progress.setRange(0, 30)
|
||||
self._progress.setTextVisible(False)
|
||||
self._progress.setFixedHeight(6)
|
||||
self._progress.setStyleSheet(
|
||||
"QProgressBar { border-radius: 3px; background: #ecf0f1; }"
|
||||
"QProgressBar::chunk { background: #3498db; border-radius: 3px; }"
|
||||
)
|
||||
layout.addWidget(self._progress)
|
||||
|
||||
self._timer_label = QLabel("")
|
||||
self._timer_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self._timer_label.setStyleSheet("color: #999; font-size: 11px;")
|
||||
layout.addWidget(self._timer_label)
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
btn_cancel = QPushButton("Annuler")
|
||||
btn_cancel.setStyleSheet(
|
||||
"QPushButton { padding: 8px 16px; border-radius: 5px; "
|
||||
"background: #ecf0f1; } QPushButton:hover { background: #bdc3c7; }"
|
||||
)
|
||||
btn_cancel.clicked.connect(self.reject)
|
||||
|
||||
self._btn_ok = QPushButton("Vérifier")
|
||||
self._btn_ok.setDefault(True)
|
||||
self._btn_ok.setStyleSheet(
|
||||
"QPushButton { padding: 8px 20px; border-radius: 5px; "
|
||||
"background: #3498db; color: white; font-weight: bold; }"
|
||||
"QPushButton:hover { background: #2980b9; }"
|
||||
)
|
||||
self._btn_ok.clicked.connect(self._verify)
|
||||
btn_row.addWidget(btn_cancel)
|
||||
btn_row.addStretch()
|
||||
btn_row.addWidget(self._btn_ok)
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
def _tick(self):
|
||||
remaining = mfa_core.time_remaining()
|
||||
self._progress.setValue(remaining)
|
||||
self._timer_label.setText(f"Code valide encore {remaining}s")
|
||||
|
||||
def _verify(self):
|
||||
raw = self._code_input.text().replace(" ", "").replace("-", "")
|
||||
if len(raw) != 6 or not raw.isdigit():
|
||||
self._error_label.setText("Entrez exactement 6 chiffres.")
|
||||
self._code_input.setStyleSheet(
|
||||
"padding: 8px; border: 2px solid #e74c3c; border-radius: 6px;"
|
||||
)
|
||||
return
|
||||
if mfa_core.verify_code(self._secret, raw):
|
||||
self._verified = True
|
||||
self.accept()
|
||||
else:
|
||||
self._error_label.setText("Code incorrect. Réessayez.")
|
||||
self._code_input.clear()
|
||||
self._code_input.setStyleSheet(
|
||||
"padding: 8px; border: 2px solid #e74c3c; border-radius: 6px;"
|
||||
)
|
||||
|
||||
def is_verified(self) -> bool:
|
||||
return self._verified
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._timer.stop()
|
||||
super().closeEvent(event)
|
||||
@@ -0,0 +1,97 @@
|
||||
from PyQt6.QtWidgets import QSystemTrayIcon, QMenu, QApplication
|
||||
from PyQt6.QtGui import QAction
|
||||
from PyQt6.QtCore import QTimer
|
||||
|
||||
from app.core.config import Config
|
||||
from app.core import wireguard as wg_core
|
||||
from app.ui import icons
|
||||
|
||||
|
||||
class SystemTray(QSystemTrayIcon):
|
||||
def __init__(self, config: Config, main_window, parent=None):
|
||||
super().__init__(icons.icon_disconnected(), parent)
|
||||
self._cfg = config
|
||||
self._win = main_window
|
||||
self._connected = False
|
||||
self._build_menu()
|
||||
self.setToolTip("WGSecure — Déconnecté")
|
||||
self.activated.connect(self._on_activated)
|
||||
|
||||
self._poll = QTimer(parent)
|
||||
self._poll.timeout.connect(self._update_status)
|
||||
self._poll.start(5000)
|
||||
self._update_status()
|
||||
|
||||
def _build_menu(self):
|
||||
menu = QMenu()
|
||||
|
||||
# Ligne de statut (désactivée)
|
||||
self._action_status = QAction("● Déconnecté", self)
|
||||
self._action_status.setEnabled(False)
|
||||
menu.addAction(self._action_status)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
# Afficher la fenêtre
|
||||
self._action_show = QAction(icons.icon_app(), "Afficher la fenêtre", self)
|
||||
self._action_show.triggered.connect(self._show_window)
|
||||
menu.addAction(self._action_show)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
# Connexion / déconnexion
|
||||
self._action_toggle = QAction(icons.icon_disconnected(), "Se connecter", self)
|
||||
self._action_toggle.triggered.connect(self._toggle_connection)
|
||||
menu.addAction(self._action_toggle)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
# Panneau de configuration
|
||||
self._action_config = QAction(icons.icon_admin(), "Configuration", self)
|
||||
self._action_config.triggered.connect(self._win._open_admin)
|
||||
menu.addAction(self._action_config)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
# Quitter
|
||||
self._action_quit = QAction("Quitter", self)
|
||||
self._action_quit.triggered.connect(self._quit)
|
||||
menu.addAction(self._action_quit)
|
||||
|
||||
self.setContextMenu(menu)
|
||||
|
||||
def _on_activated(self, reason: QSystemTrayIcon.ActivationReason):
|
||||
if reason == QSystemTrayIcon.ActivationReason.Trigger:
|
||||
self._show_window()
|
||||
|
||||
def _show_window(self):
|
||||
self._win.show()
|
||||
self._win.raise_()
|
||||
self._win.activateWindow()
|
||||
|
||||
def _update_status(self):
|
||||
try:
|
||||
self._connected = wg_core.is_connected(self._cfg)
|
||||
except Exception:
|
||||
self._connected = False
|
||||
|
||||
if self._connected:
|
||||
self.setIcon(icons.icon_connected())
|
||||
self.setToolTip("WGSecure — Connecté")
|
||||
self._action_status.setText("● Connecté")
|
||||
self._action_toggle.setIcon(icons.icon_connected())
|
||||
self._action_toggle.setText("Se déconnecter")
|
||||
else:
|
||||
self.setIcon(icons.icon_disconnected())
|
||||
self.setToolTip("WGSecure — Déconnecté")
|
||||
self._action_status.setText("● Déconnecté")
|
||||
self._action_toggle.setIcon(icons.icon_disconnected())
|
||||
self._action_toggle.setText("Se connecter")
|
||||
|
||||
def _toggle_connection(self):
|
||||
self._win._on_connect()
|
||||
self._update_status()
|
||||
|
||||
def _quit(self):
|
||||
self._poll.stop()
|
||||
QApplication.quit()
|
||||
Reference in New Issue
Block a user