298 lines
12 KiB
Python
298 lines
12 KiB
Python
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()
|