fix(ui): la saisie du code ne dépend plus du champ Identifiant VPN (v0.8.4)
Le bouton et l'entrée de systray étaient conditionnés à un champ vide par défaut depuis son introduction en v0.8.0 : un poste tenu en quarantaine par le serveur n'affichait donc aucun moyen d'en sortir. La saisie ne dépend plus que de l'état du tunnel ; la fenêtre demande l'identifiant quand il manque et ne l'enregistre qu'une fois le serveur l'ayant accepté. L'état « Non géré par le serveur » disparaît au passage : le client l'affirmait à partir de sa seule configuration locale, sans rien vérifier. « Non vérifié » est la vérité tant qu'aucun code n'a été validé. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,17 @@ Ce projet suit le [Versionnage Sémantique](https://semver.org/lang/fr/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.8.4] — 2026-09-06
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
- **Aucun moyen de saisir le code tant que le champ *Identifiant VPN* n'était pas renseigné** — ni bouton, ni entrée de systray, sur les deux plateformes. Ce champ, introduit en v0.8.0 et vide par défaut, conditionnait l'affichage des deux commandes : un poste que le serveur tenait bel et bien en quarantaine n'affichait donc rien pour en sortir, précisément dans la situation où il en avait besoin. La saisie ne dépend plus que du tunnel : monté et accès non ouvert, elle est atteignable. L'identifiant est demandé dans la fenêtre elle-même quand il manque, et enregistré une fois que le serveur l'a reconnu — jamais avant.
|
||||||
|
- **L'état « Non géré par le serveur » était une affirmation sans vérification.** Le client la tirait de sa seule configuration locale, alors qu'il n'en sait rien : le serveur peut parfaitement tenir ce pair en quarantaine pendant que le poste ne mentionne aucun serveur. La ligne indique désormais « Non vérifié », qui est la vérité tant qu'aucun code n'a été validé.
|
||||||
|
|
||||||
|
### Modifié
|
||||||
|
- L'invite spontanée après connexion reste, elle, conditionnée à un identifiant connu : une installation sans serveur d'authentification n'a aucune raison de voir surgir cette fenêtre à chaque connexion. Le bouton reflète la nuance — mis en avant quand un code est réellement attendu, discret sinon.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.8.3] — 2026-09-06
|
## [0.8.3] — 2026-09-06
|
||||||
|
|
||||||
### Corrigé
|
### Corrigé
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
## ──────────────────────────────────────────────
|
## ──────────────────────────────────────────────
|
||||||
|
|
||||||
APP := wgsecure
|
APP := wgsecure
|
||||||
VERSION := 0.8.3
|
VERSION := 0.8.4
|
||||||
VENV := .venv
|
VENV := .venv
|
||||||
PYTHON := $(VENV)/bin/python3
|
PYTHON := $(VENV)/bin/python3
|
||||||
PIP := $(VENV)/bin/pip
|
PIP := $(VENV)/bin/pip
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 🛡️ WGSecure (WGS)
|
# 🛡️ WGSecure (WGS)
|
||||||
|
|
||||||
🚀 v0.8.3 · 🐍 · 🪟🐧 · 🔐 Accès validé par le serveur · 🛡️ WireGuard
|
🚀 v0.8.4 · 🐍 · 🪟🐧 · 🔐 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**.
|
**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**.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
__version__ = "0.8.3"
|
__version__ = "0.8.4"
|
||||||
APP_NAME = "WGSecure"
|
APP_NAME = "WGSecure"
|
||||||
APP_SHORT = "WGS"
|
APP_SHORT = "WGS"
|
||||||
|
|||||||
@@ -52,23 +52,28 @@ def api_base_url(cfg: Config) -> str:
|
|||||||
return f"http://{gateway}:8000"
|
return f"http://{gateway}:8000"
|
||||||
|
|
||||||
|
|
||||||
def authenticate(cfg: Config, code: str) -> tuple[bool, str]:
|
def authenticate(cfg: Config, code: str, username: str = "") -> tuple[bool, str]:
|
||||||
"""Valide un code TOTP auprès du serveur et ouvre l'accès réseau.
|
"""Valide un code TOTP auprès du serveur et ouvre l'accès réseau.
|
||||||
|
|
||||||
Retourne (succès, message). Un échec n'est jamais silencieux : sans
|
Retourne (succès, message). Un échec n'est jamais silencieux : sans
|
||||||
cette validation le tunnel reste monté mais ne mène nulle part, et
|
cette validation le tunnel reste monté mais ne mène nulle part, et
|
||||||
l'utilisateur doit comprendre pourquoi plutôt que de constater un
|
l'utilisateur doit comprendre pourquoi plutôt que de constater un
|
||||||
réseau inexplicablement muet.
|
réseau inexplicablement muet.
|
||||||
|
|
||||||
|
`username` permet de fournir l'identifiant à la volée, quand il n'est pas
|
||||||
|
encore en configuration : la fenêtre de saisie le demande alors avec le
|
||||||
|
code, plutôt que de renvoyer l'utilisateur vers le panneau Administrateur
|
||||||
|
au moment précis où il cherche à ouvrir son accès. Il est enregistré
|
||||||
|
une fois le serveur l'ayant accepté.
|
||||||
"""
|
"""
|
||||||
base = api_base_url(cfg)
|
base = api_base_url(cfg)
|
||||||
if not base:
|
if not base:
|
||||||
return False, ("Adresse du serveur d'authentification inconnue. "
|
return False, ("Adresse du serveur d'authentification inconnue. "
|
||||||
"Renseignez-la dans le panneau Administrateur.")
|
"Renseignez-la dans le panneau Administrateur.")
|
||||||
|
|
||||||
username = (cfg.wg.get("vpn_username") or "").strip()
|
username = (username or cfg.wg.get("vpn_username") or "").strip()
|
||||||
if not username:
|
if not username:
|
||||||
return False, ("Aucun identifiant VPN configuré. Renseignez-le dans "
|
return False, "Indiquez votre identifiant VPN."
|
||||||
"le panneau Administrateur.")
|
|
||||||
|
|
||||||
payload = json.dumps({"username": username, "code": code}).encode("utf-8")
|
payload = json.dumps({"username": username, "code": code}).encode("utf-8")
|
||||||
request = urllib.request.Request(
|
request = urllib.request.Request(
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class AccessCodeDialog(QDialog):
|
|||||||
self._cfg = cfg
|
self._cfg = cfg
|
||||||
self._worker: TaskWorker | None = None
|
self._worker: TaskWorker | None = None
|
||||||
self._message = ""
|
self._message = ""
|
||||||
|
self._username = ""
|
||||||
self.setWindowTitle("WGSecure — Code d'accès")
|
self.setWindowTitle("WGSecure — Code d'accès")
|
||||||
self.setFixedWidth(360)
|
self.setFixedWidth(360)
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
@@ -83,6 +84,19 @@ class AccessCodeDialog(QDialog):
|
|||||||
sep.setStyleSheet(f"color: {theme.BORDER_SOFT};")
|
sep.setStyleSheet(f"color: {theme.BORDER_SOFT};")
|
||||||
layout.addWidget(sep)
|
layout.addWidget(sep)
|
||||||
|
|
||||||
|
# L'identifiant n'est demandé que s'il manque encore. Le renvoyer au
|
||||||
|
# panneau Administrateur à cet instant précis — tunnel monté, accès
|
||||||
|
# fermé, code en main — était un cul-de-sac : il est saisi ici, puis
|
||||||
|
# enregistré dès que le serveur l'a reconnu.
|
||||||
|
self._user_input: QLineEdit | None = None
|
||||||
|
if not (self._cfg.wg.get("vpn_username") or "").strip():
|
||||||
|
self._user_input = QLineEdit()
|
||||||
|
self._user_input.setPlaceholderText("Identifiant VPN (ex. TestB)")
|
||||||
|
self._user_input.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self._user_input.setStyleSheet(self._input_css(theme.BORDER_SOFT))
|
||||||
|
self._user_input.returnPressed.connect(self._submit)
|
||||||
|
layout.addWidget(self._user_input)
|
||||||
|
|
||||||
self._code_input = QLineEdit()
|
self._code_input = QLineEdit()
|
||||||
self._code_input.setPlaceholderText("000 000")
|
self._code_input.setPlaceholderText("000 000")
|
||||||
self._code_input.setMaxLength(7)
|
self._code_input.setMaxLength(7)
|
||||||
@@ -170,10 +184,18 @@ class AccessCodeDialog(QDialog):
|
|||||||
self._fail("Entrez exactement 6 chiffres.")
|
self._fail("Entrez exactement 6 chiffres.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._username = ""
|
||||||
|
if self._user_input is not None:
|
||||||
|
self._username = self._user_input.text().strip()
|
||||||
|
if not self._username:
|
||||||
|
self._error_label.setText("Indiquez votre identifiant VPN.")
|
||||||
|
self._user_input.setStyleSheet(self._input_css(theme.FAIL_TEXT))
|
||||||
|
return
|
||||||
|
|
||||||
self._error_label.setText("")
|
self._error_label.setText("")
|
||||||
self._set_busy(True)
|
self._set_busy(True)
|
||||||
self._worker = TaskWorker(vpn_session.authenticate, self._cfg, raw,
|
self._worker = TaskWorker(vpn_session.authenticate, self._cfg, raw,
|
||||||
tag="auth", parent=self)
|
self._username, tag="auth", parent=self)
|
||||||
self._worker.done.connect(self._on_reply)
|
self._worker.done.connect(self._on_reply)
|
||||||
self._worker.finished.connect(self._worker.deleteLater)
|
self._worker.finished.connect(self._worker.deleteLater)
|
||||||
self._worker.start()
|
self._worker.start()
|
||||||
@@ -182,6 +204,11 @@ class AccessCodeDialog(QDialog):
|
|||||||
self._worker = None
|
self._worker = None
|
||||||
self._message = message
|
self._message = message
|
||||||
if ok:
|
if ok:
|
||||||
|
# Enregistré seulement maintenant : un identifiant que le serveur
|
||||||
|
# a refusé n'a rien à faire dans la configuration.
|
||||||
|
if self._username:
|
||||||
|
self._cfg.set("wg", "vpn_username", self._username)
|
||||||
|
self._cfg.save()
|
||||||
self.accept()
|
self.accept()
|
||||||
return
|
return
|
||||||
self._set_busy(False)
|
self._set_busy(False)
|
||||||
|
|||||||
+51
-25
@@ -247,14 +247,10 @@ class MainWindow(QMainWindow):
|
|||||||
# rien ajouter aux installations sans authentification serveur.
|
# rien ajouter aux installations sans authentification serveur.
|
||||||
self._btn_access = QPushButton("🔐 Saisir le code d'accès")
|
self._btn_access = QPushButton("🔐 Saisir le code d'accès")
|
||||||
self._btn_access.setFixedHeight(32)
|
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.clicked.connect(self.request_access_code)
|
||||||
self._btn_access.setVisible(False)
|
self._btn_access.setVisible(False)
|
||||||
s.addWidget(self._btn_access)
|
s.addWidget(self._btn_access)
|
||||||
|
self._style_access_button(False)
|
||||||
|
|
||||||
# Repli/dépli du détail (transfert, config, bande passante, journal) :
|
# Repli/dépli du détail (transfert, config, bande passante, journal) :
|
||||||
# rattaché à l'action qu'il détaille, pas à l'en-tête.
|
# rattaché à l'action qu'il détaille, pas à l'en-tête.
|
||||||
@@ -370,6 +366,18 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
self._sync_window_height()
|
self._sync_window_height()
|
||||||
|
|
||||||
|
def _style_access_button(self, expected: bool):
|
||||||
|
"""Met le bouton en avant seulement quand un code est réellement dû."""
|
||||||
|
bg, hover = ((theme.WARN_SOLID, "#d68910") if expected
|
||||||
|
else (theme.BG_RAISED, theme.ACCENT_HOVER))
|
||||||
|
weight = "bold" if expected else "normal"
|
||||||
|
self._btn_access.setStyleSheet(
|
||||||
|
f"QPushButton {{ background: {bg}; color: {theme.TEXT};"
|
||||||
|
f" font-size: 12px; font-weight: {weight}; border-radius: 6px;"
|
||||||
|
" border: none; }"
|
||||||
|
f"QPushButton:hover {{ background: {hover}; }}"
|
||||||
|
)
|
||||||
|
|
||||||
def _sync_window_height(self):
|
def _sync_window_height(self):
|
||||||
"""Réajuste la hauteur fixe de la fenêtre à son contenu visible.
|
"""Réajuste la hauteur fixe de la fenêtre à son contenu visible.
|
||||||
|
|
||||||
@@ -591,12 +599,19 @@ class MainWindow(QMainWindow):
|
|||||||
tant que le serveur n'a pas levé la quarantaine. Sans cette ligne,
|
tant que le serveur n'a pas levé la quarantaine. Sans cette ligne,
|
||||||
un utilisateur ayant refusé la saisie du code verrait « Connecté »
|
un utilisateur ayant refusé la saisie du code verrait « Connecté »
|
||||||
et un réseau inexplicablement muet.
|
et un réseau inexplicablement muet.
|
||||||
|
|
||||||
|
Sans identifiant VPN, l'état est « non vérifié » et non « non géré » :
|
||||||
|
le client n'en sait rien. Il l'affirmait, et se trompait — le serveur
|
||||||
|
peut parfaitement tenir ce pair en quarantaine pendant que la
|
||||||
|
configuration locale, elle, ne mentionne aucun serveur.
|
||||||
"""
|
"""
|
||||||
if not (self._cfg.wg.get("vpn_username") or "").strip():
|
|
||||||
return "Non géré par le serveur"
|
|
||||||
if not self._is_connected:
|
if not self._is_connected:
|
||||||
return "—"
|
return "—"
|
||||||
return "Ouvert ✓" if self._access_open else "Quarantaine (code requis)"
|
if self._access_open:
|
||||||
|
return "Ouvert ✓"
|
||||||
|
if (self._cfg.wg.get("vpn_username") or "").strip():
|
||||||
|
return "Quarantaine (code requis)"
|
||||||
|
return "Non vérifié"
|
||||||
|
|
||||||
def _refresh_status(self):
|
def _refresh_status(self):
|
||||||
wg = self._cfg.wg
|
wg = self._cfg.wg
|
||||||
@@ -638,6 +653,13 @@ class MainWindow(QMainWindow):
|
|||||||
if needed == self._btn_access.isHidden():
|
if needed == self._btn_access.isHidden():
|
||||||
self._btn_access.setVisible(needed)
|
self._btn_access.setVisible(needed)
|
||||||
self._sync_window_height()
|
self._sync_window_height()
|
||||||
|
if needed:
|
||||||
|
# Un code est attendu de façon certaine quand l'identifiant VPN
|
||||||
|
# est connu ; sinon le bouton reste disponible mais discret, pour
|
||||||
|
# ne pas réclamer une action à une installation qui n'a pas de
|
||||||
|
# serveur d'authentification.
|
||||||
|
self._style_access_button(
|
||||||
|
bool((self._cfg.wg.get("vpn_username") or "").strip()))
|
||||||
|
|
||||||
if connected:
|
if connected:
|
||||||
if self._connected_since is None:
|
if self._connected_since is None:
|
||||||
@@ -734,12 +756,17 @@ class MainWindow(QMainWindow):
|
|||||||
def access_code_needed(self) -> bool:
|
def access_code_needed(self) -> bool:
|
||||||
"""Vrai quand une saisie de code aurait un sens ici et maintenant.
|
"""Vrai quand une saisie de code aurait un sens ici et maintenant.
|
||||||
|
|
||||||
Trois conditions : un identifiant VPN configuré (sans quoi le serveur
|
Deux conditions seulement : un tunnel monté — l'API du serveur n'est
|
||||||
ne gère pas ce poste), un tunnel monté (l'API n'est joignable que par
|
joignable que par l'intérieur — et aucune autorisation en cours.
|
||||||
l'intérieur) et aucune autorisation en cours.
|
|
||||||
|
L'identifiant VPN n'en fait volontairement pas partie. Il en faisait
|
||||||
|
partie, et c'était un piège : un poste bel et bien tenu en quarantaine
|
||||||
|
par le serveur, mais dont ce champ n'avait jamais été renseigné,
|
||||||
|
n'affichait aucun moyen de saisir son code — précisément dans la
|
||||||
|
situation où il en avait besoin. La fenêtre de saisie demande
|
||||||
|
l'identifiant quand il manque.
|
||||||
"""
|
"""
|
||||||
return (bool((self._cfg.wg.get("vpn_username") or "").strip())
|
return self._is_connected and not self._access_open
|
||||||
and self._is_connected and not self._access_open)
|
|
||||||
|
|
||||||
def request_access_code(self):
|
def request_access_code(self):
|
||||||
"""Ouvre la saisie du code, à la demande de l'utilisateur.
|
"""Ouvre la saisie du code, à la demande de l'utilisateur.
|
||||||
@@ -755,21 +782,17 @@ class MainWindow(QMainWindow):
|
|||||||
"Le serveur d'authentification n'est joignable que depuis "
|
"Le serveur d'authentification n'est joignable que depuis "
|
||||||
"l'intérieur du tunnel.\nConnectez-vous d'abord.")
|
"l'intérieur du tunnel.\nConnectez-vous d'abord.")
|
||||||
return
|
return
|
||||||
if not (self._cfg.wg.get("vpn_username") or "").strip():
|
self._open_access_dialog()
|
||||||
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):
|
def _authenticate_with_server(self, silent: bool):
|
||||||
"""Demande le code à 6 chiffres et le fait valider par le serveur.
|
"""Invite spontanée à saisir le code, juste après une connexion.
|
||||||
|
|
||||||
Sans identifiant VPN configuré, l'étape est ignorée : le serveur
|
Sans identifiant VPN configuré, l'invite est tue : le serveur peut
|
||||||
peut très bien tourner en mode permissif, où un compte non enrôlé
|
très bien tourner en mode permissif, où un compte non enrôlé conserve
|
||||||
conserve son accès. Imposer la saisie dans ce cas bloquerait des
|
son accès, et une installation sans serveur d'authentification n'a
|
||||||
installations qui fonctionnent parfaitement.
|
aucune raison de voir surgir cette fenêtre à chaque connexion. Cela
|
||||||
|
ne vaut que pour l'invite : la saisie reste atteignable à la demande,
|
||||||
|
par le bouton et par le systray, quelle que soit la configuration.
|
||||||
"""
|
"""
|
||||||
if not (self._cfg.wg.get("vpn_username") or "").strip():
|
if not (self._cfg.wg.get("vpn_username") or "").strip():
|
||||||
return
|
return
|
||||||
@@ -780,7 +803,10 @@ class MainWindow(QMainWindow):
|
|||||||
self._add_log("Code d'accès requis — bouton « Saisir le code "
|
self._add_log("Code d'accès requis — bouton « Saisir le code "
|
||||||
"d'accès »", "warning")
|
"d'accès »", "warning")
|
||||||
return
|
return
|
||||||
|
self._open_access_dialog()
|
||||||
|
|
||||||
|
def _open_access_dialog(self):
|
||||||
|
"""Ouvre la fenêtre de saisie et journalise la réponse du serveur."""
|
||||||
dlg = AccessCodeDialog(self._cfg, self)
|
dlg = AccessCodeDialog(self._cfg, self)
|
||||||
accepted = dlg.exec() == AccessCodeDialog.DialogCode.Accepted
|
accepted = dlg.exec() == AccessCodeDialog.DialogCode.Accepted
|
||||||
self._access_open = accepted
|
self._access_open = accepted
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
; ──────────────────────────────────────────────
|
; ──────────────────────────────────────────────
|
||||||
|
|
||||||
#define MyAppName "WGSecure"
|
#define MyAppName "WGSecure"
|
||||||
#define MyAppVersion "0.8.3"
|
#define MyAppVersion "0.8.4"
|
||||||
#define MyAppPublisher "WGSecure"
|
#define MyAppPublisher "WGSecure"
|
||||||
#define MyAppExeName "wgsecure.exe"
|
#define MyAppExeName "wgsecure.exe"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user