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:
2026-09-06 19:32:27 +02:00
co-authored by Claude Opus 5
parent 1e015b5a1f
commit 9083525634
8 changed files with 103 additions and 34 deletions
+51 -25
View File
@@ -247,14 +247,10 @@ class MainWindow(QMainWindow):
# 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)
self._style_access_button(False)
# Repli/dépli du détail (transfert, config, bande passante, journal) :
# rattaché à l'action qu'il détaille, pas à l'en-tête.
@@ -370,6 +366,18 @@ class MainWindow(QMainWindow):
)
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):
"""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,
un utilisateur ayant refusé la saisie du code verrait « Connecté »
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:
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):
wg = self._cfg.wg
@@ -638,6 +653,13 @@ class MainWindow(QMainWindow):
if needed == self._btn_access.isHidden():
self._btn_access.setVisible(needed)
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 self._connected_since is None:
@@ -734,12 +756,17 @@ class MainWindow(QMainWindow):
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.
Deux conditions seulement : un tunnel monté — l'API du serveur n'est
joignable que par 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())
and self._is_connected and not self._access_open)
return self._is_connected and not self._access_open
def request_access_code(self):
"""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 "
"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)
self._open_access_dialog()
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
peut très bien tourner en mode permissif, où un compte non enrôlé
conserve son accès. Imposer la saisie dans ce cas bloquerait des
installations qui fonctionnent parfaitement.
Sans identifiant VPN configuré, l'invite est tue : le serveur peut
très bien tourner en mode permissif, où un compte non enrôlé conserve
son accès, et une installation sans serveur d'authentification n'a
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():
return
@@ -780,7 +803,10 @@ class MainWindow(QMainWindow):
self._add_log("Code d'accès requis — bouton « Saisir le code "
"d'accès »", "warning")
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)
accepted = dlg.exec() == AccessCodeDialog.DialogCode.Accepted
self._access_open = accepted