diff --git a/CHANGELOG.md b/CHANGELOG.md index 207bc4c..70b1a12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ Ce projet suit le [Versionnage Sémantique](https://semver.org/lang/fr/). --- +## [0.8.2] — 2026-09-06 + +### Corrigé +- **Aucun moyen de saisir son code une fois en quarantaine.** La fenêtre de saisie ne s'ouvrait qu'une fois, dans la seconde suivant une connexion manuelle réussie. Un code refusé, une fenêtre fermée, une reconnexion automatique — qui n'ouvre volontairement aucune fenêtre modale, personne n'étant devant l'écran — ou l'expiration de l'autorisation au bout de 12 h laissaient l'utilisateur avec un tunnel monté, un réseau muet et pour seule issue une déconnexion/reconnexion. Un bouton **« Saisir le code d'accès »** apparaît désormais sous le bouton de connexion, et une entrée équivalente dans le menu du systray, dès qu'une saisie a un sens : identifiant VPN configuré, tunnel monté, accès non ouvert. Ils disparaissent le reste du temps, sans rien changer aux installations sans authentification serveur. +- Le bouton apparaissait et disparaissait correctement, mais son calcul de hauteur se relançait à chaque cycle de rafraîchissement lorsque la fenêtre était repliée dans le systray : `isVisible()` y est faux pour tout enfant d'une fenêtre masquée, quel que soit l'état demandé. C'est `isHidden()` qui porte cette intention. + +--- + ## [0.8.1] — 2026-09-06 ### Sécurité diff --git a/Makefile b/Makefile index 9fdeb4d..7dd4dfa 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ ## ────────────────────────────────────────────── APP := wgsecure -VERSION := 0.8.1 +VERSION := 0.8.2 VENV := .venv PYTHON := $(VENV)/bin/python3 PIP := $(VENV)/bin/pip diff --git a/README.md b/README.md index 09dd3d2..6ae4926 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🛡️ WGSecure (WGS) -🚀 v0.8.1 · 🐍 · 🪟🐧 · 🔐 Accès validé par le serveur · 🛡️ WireGuard +🚀 v0.8.2 · 🐍 · 🪟🐧 · 🔐 Accès validé par le serveur · 🛡️ WireGuard **WGSecure** est une interface graphique multiplateforme (Windows & Linux) pour gérer une connexion WireGuard dont l'accès aux ressources distantes est ouvert par un code à 6 chiffres validé **par le serveur**. diff --git a/app/__init__.py b/app/__init__.py index af2d0b0..e9c9d9b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,3 +1,3 @@ -__version__ = "0.8.1" +__version__ = "0.8.2" APP_NAME = "WGSecure" APP_SHORT = "WGS" diff --git a/app/ui/main_window.py b/app/ui/main_window.py index 918ebc8..b9b7fb2 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -238,6 +238,23 @@ class MainWindow(QMainWindow): self._btn_connect.clicked.connect(self._on_connect) s.addWidget(self._btn_connect) + # Saisie du code d'accès, atteignable à tout moment. Le tunnel monte + # en quarantaine et l'autorisation obtenue expire d'elle-même : sans + # ce bouton, un code refusé, une reconnexion automatique ou une + # autorisation échue ne laissaient d'autre issue que se déconnecter + # puis se reconnecter. Masqué tant qu'il n'y a rien à saisir, pour ne + # rien ajouter aux installations sans authentification serveur. + self._btn_access = QPushButton("🔐 Saisir le code d'accès") + self._btn_access.setFixedHeight(32) + self._btn_access.setStyleSheet( + f"QPushButton {{ background: {theme.WARN_SOLID}; color: {theme.TEXT};" + " font-size: 12px; font-weight: bold; border-radius: 6px; border: none; }" + "QPushButton:hover { background: #d68910; }" + ) + self._btn_access.clicked.connect(self.request_access_code) + self._btn_access.setVisible(False) + s.addWidget(self._btn_access) + # Repli/dépli du détail (transfert, config, bande passante, journal) : # rattaché à l'action qu'il détaille, pas à l'en-tête. self._btn_toggle_details = QPushButton("▾ Afficher le détail") @@ -599,6 +616,16 @@ class MainWindow(QMainWindow): self._access_open = False self._info_labels["access"].setText(self._access_state()) + # Le bouton n'apparaît que lorsqu'il y a réellement un code à saisir ; + # sa hauteur entre et sort du calcul de la fenêtre, d'où le + # réajustement. `isHidden()` et non `isVisible()` : cette dernière est + # fausse dès que la fenêtre est repliée dans le systray, ce qui + # relancerait le calcul de hauteur à chaque cycle de rafraîchissement. + needed = self.access_code_needed() + if needed == self._btn_access.isHidden(): + self._btn_access.setVisible(needed) + self._sync_window_height() + if connected: if self._connected_since is None: self._connected_since = QDateTime.currentDateTime() @@ -691,6 +718,38 @@ class MainWindow(QMainWindow): self._worker.finished.connect(self._worker.deleteLater) self._worker.start() + def access_code_needed(self) -> bool: + """Vrai quand une saisie de code aurait un sens ici et maintenant. + + Trois conditions : un identifiant VPN configuré (sans quoi le serveur + ne gère pas ce poste), un tunnel monté (l'API n'est joignable que par + l'intérieur) et aucune autorisation en cours. + """ + return (bool((self._cfg.wg.get("vpn_username") or "").strip()) + and self._is_connected and not self._access_open) + + def request_access_code(self): + """Ouvre la saisie du code, à la demande de l'utilisateur. + + Distincte de l'enchaînement automatique après connexion : le tunnel + monte en quarantaine et l'autorisation expire d'elle-même, il faut + donc pouvoir revenir saisir un code à tout moment — après un refus, + après une reconnexion automatique, ou une fois l'autorisation échue. + """ + if not self._is_connected: + QMessageBox.information( + self, "Tunnel inactif", + "Le serveur d'authentification n'est joignable que depuis " + "l'intérieur du tunnel.\nConnectez-vous d'abord.") + return + if not (self._cfg.wg.get("vpn_username") or "").strip(): + QMessageBox.information( + self, "Aucun identifiant VPN", + "Renseignez l'identifiant VPN connu du serveur dans le " + "panneau Administrateur.") + return + self._authenticate_with_server(silent=False) + def _authenticate_with_server(self, silent: bool): """Demande le code à 6 chiffres et le fait valider par le serveur. @@ -703,9 +762,10 @@ class MainWindow(QMainWindow): return if silent: # Reconnexion automatique : ouvrir une fenêtre modale sans - # personne devant l'écran laisserait l'application figée. - self._add_log("Code d'accès requis — reconnectez-vous manuellement", - "warning") + # personne devant l'écran laisserait l'application figée. Le + # bouton « Saisir le code d'accès » prend le relais. + self._add_log("Code d'accès requis — bouton « Saisir le code " + "d'accès »", "warning") return dlg = AccessCodeDialog(self._cfg, self) diff --git a/app/ui/systray.py b/app/ui/systray.py index 576700e..3412ccc 100644 --- a/app/ui/systray.py +++ b/app/ui/systray.py @@ -44,6 +44,15 @@ class SystemTray(QSystemTrayIcon): self._action_toggle.triggered.connect(self._toggle_connection) menu.addAction(self._action_toggle) + # Saisie du code d'accès : le tunnel monte en quarantaine et + # l'autorisation expire d'elle-même, il faut donc pouvoir y revenir + # sans passer par une déconnexion. Masquée quand elle n'a pas lieu + # d'être — pas d'identifiant VPN, tunnel à l'arrêt, accès déjà ouvert. + self._action_access = QAction("🔐 Saisir le code d'accès", self) + self._action_access.triggered.connect(self._win.request_access_code) + self._action_access.setVisible(False) + menu.addAction(self._action_access) + menu.addSeparator() # Panneau de configuration @@ -76,6 +85,11 @@ class SystemTray(QSystemTrayIcon): except Exception: self._connected = False + # La fenêtre principale est seule à savoir si le serveur a ouvert + # l'accès : elle tient cet état, qu'aucune commande WireGuard ne + # rapporte — le tunnel est identique, ouvert ou en quarantaine. + self._action_access.setVisible(self._win.access_code_needed()) + if self._connected: self.setIcon(icons.icon_connected()) self.setToolTip("WGSecure — Connecté") diff --git a/wgsecure.iss b/wgsecure.iss index a4fc6d5..7e44fb1 100644 --- a/wgsecure.iss +++ b/wgsecure.iss @@ -5,7 +5,7 @@ ; ────────────────────────────────────────────── #define MyAppName "WGSecure" -#define MyAppVersion "0.8.1" +#define MyAppVersion "0.8.2" #define MyAppPublisher "WGSecure" #define MyAppExeName "wgsecure.exe"