Initial release

This commit is contained in:
2026-06-01 22:31:22 +02:00
parent ab5ba103fe
commit 0d2646a9ad
17 changed files with 1831 additions and 0 deletions
View File
+535
View File
@@ -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
View File
@@ -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)
+297
View File
@@ -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()
+130
View File
@@ -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)
+97
View File
@@ -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()