feat(ui): trois sous-onglets de plus dans « À propos »
Mode d'emploi, Prérequis et Sécurité rejoignent Fonctions, Garanties, Technique, Journal et Auteur. Le contenu vit dans app_info.py, séparé de sa présentation : le relire ou le corriger ne demande pas de traverser le code de l'interface. Deux aides — _about_group_label et _about_grouped — découpent en groupes titrés les listes d'une vingtaine de cartes, sans quoi Prérequis et Sécurité se lisaient comme un seul bloc indifférencié. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a989f50151
commit
205258cbf1
+258
@@ -118,6 +118,264 @@ TECH_STACK = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Mode d'emploi ────────────────────────────────────────────────────────
|
||||||
|
HOWTO_FIRST_INTRO = (
|
||||||
|
"Huit étapes entre une installation nue et un tunnel qui monte. "
|
||||||
|
"L'ordre compte : le serveur ne peut pas accepter le poste tant qu'il "
|
||||||
|
"n'a pas reçu sa clé publique."
|
||||||
|
)
|
||||||
|
HOWTO_FIRST = [
|
||||||
|
"Ouvrir <b>⚙️ Administrateur</b> depuis la fenêtre principale.",
|
||||||
|
"Onglet <b>WireGuard → Tunnel & profils</b> : renseigner l'adresse du "
|
||||||
|
"serveur, le port UDP et la clé publique du serveur.",
|
||||||
|
"Onglet <b>WireGuard → Clés</b> : cliquer sur <b>Générer une nouvelle "
|
||||||
|
"paire de clés</b>, puis transmettre la <b>clé publique</b> affichée à "
|
||||||
|
"l'administrateur du serveur — sans elle, le serveur refusera la "
|
||||||
|
"connexion.",
|
||||||
|
"Retour sur <b>Tunnel & profils</b> : saisir l'adresse IP du client "
|
||||||
|
"et les IPs autorisées fournies par l'administrateur.",
|
||||||
|
"Cocher <b>Le serveur utilise une clé pré-partagée</b> uniquement si le "
|
||||||
|
"serveur en déclare une pour ce poste.",
|
||||||
|
"Toujours sur <b>Tunnel & profils</b>, renseigner l'<b>Identifiant "
|
||||||
|
"VPN</b> : le nom du compte tel qu'il figure sur le serveur. Laissé "
|
||||||
|
"vide, aucun code d'accès n'est demandé à la connexion — le serveur est "
|
||||||
|
"alors traité comme permissif.",
|
||||||
|
"Cliquer sur <b>Enregistrer et fermer</b>.",
|
||||||
|
"Se connecter depuis la fenêtre principale ou le menu du systray, puis "
|
||||||
|
"saisir le code d'accès à 6 chiffres quand il est demandé.",
|
||||||
|
]
|
||||||
|
|
||||||
|
HOWTO_DAILY = [
|
||||||
|
(
|
||||||
|
"Changer de serveur",
|
||||||
|
"Onglet <b>WireGuard → Tunnel & profils</b> : sélectionner un "
|
||||||
|
"profil puis <b>✅ Charger</b>.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Reprendre une configuration reçue",
|
||||||
|
"Même onglet, <b>📂 Importer un .conf</b> : clés, adresses et clé "
|
||||||
|
"pré-partagée sont repris automatiquement. Les champs issus de "
|
||||||
|
"l'import se verrouillent, pour ne pas diverger en silence du pair "
|
||||||
|
"déjà déclaré côté serveur.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Configurer un téléphone",
|
||||||
|
"<b>📱 QR Code</b>, à scanner depuis l'application WireGuard mobile.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
HOWTO_TROUBLE = [
|
||||||
|
(
|
||||||
|
"Commencer toujours par le diagnostic",
|
||||||
|
"Onglet <b>Test connexion → Chaîne de connexion</b>, puis <b>🚀 Lancer "
|
||||||
|
"le test de connexion</b>. Le rapport nomme l'étape qui bloque au "
|
||||||
|
"lieu de laisser deviner.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Le tunnel monte mais rien ne circule",
|
||||||
|
"Vérifier que les <b>IPs autorisées</b> ne contiennent pas le réseau "
|
||||||
|
"local par lequel le serveur est joint : la passerelle serait routée "
|
||||||
|
"dans le tunnel, qui a justement besoin d'elle.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Plus aucun nom ne se résout après une coupure",
|
||||||
|
"Onglet <b>Test connexion → Fuite DNS</b>, puis <b>🛠️ Réparer le DNS "
|
||||||
|
"(tunnel arrêté)</b>.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Une fenêtre d'authentification à chaque connexion",
|
||||||
|
"Onglet <b>Sécurité</b>, sous-onglet <b>Droits sudo</b> sous Linux ou "
|
||||||
|
"<b>Élévation</b> sous Windows : il nomme ce qui manque et comment "
|
||||||
|
"l'accorder.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Le code d'accès est refusé",
|
||||||
|
"Le code est validé par le serveur, pas par ce poste : un refus vient "
|
||||||
|
"du serveur et son message est repris tel quel dans le journal.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ── Prérequis ────────────────────────────────────────────────────────────
|
||||||
|
REQUIREMENTS = [
|
||||||
|
("🐧 Linux — paquets", [
|
||||||
|
(
|
||||||
|
"wireguard-tools <i>(indispensable)</i>",
|
||||||
|
"Fournit <code>wg</code> et <code>wg-quick</code>, qui montent et "
|
||||||
|
"démontent le tunnel.<br>"
|
||||||
|
"<code>sudo apt install wireguard-tools</code> — Debian, Ubuntu"
|
||||||
|
"<br><code>sudo dnf install wireguard-tools</code> — Fedora",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"iproute2 <i>(indispensable)</i>",
|
||||||
|
"<code>ip</code> sert à supprimer une interface restée en place "
|
||||||
|
"après un arrêt brutal.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"coreutils <i>(indispensable)</i>",
|
||||||
|
"<code>install</code> dépose la configuration dans "
|
||||||
|
"/etc/wireguard avec le bon propriétaire et les bonnes "
|
||||||
|
"permissions, en une seule commande privilégiée.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"resolvconf ou systemd-resolved <i>(optionnel)</i>",
|
||||||
|
"Nécessaire pour que la directive DNS du tunnel soit appliquée. "
|
||||||
|
"En leur absence, le tunnel monte sans toucher au DNS du système "
|
||||||
|
"plutôt que d'échouer à mi-course.",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
("🪟 Windows", [
|
||||||
|
(
|
||||||
|
"WireGuard for Windows <i>(indispensable)</i>",
|
||||||
|
"<code>wireguard.exe</code> installe et retire le service du "
|
||||||
|
"tunnel. L'installeur officiel est embarqué dans le programme "
|
||||||
|
"d'installation de WGSecure, qui le lance si WireGuard est absent "
|
||||||
|
"du poste.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Droits administrateur",
|
||||||
|
"Exigés pour installer ou retirer le service. Le service n'est "
|
||||||
|
"installé qu'une fois : les démarrages suivants n'ouvrent plus "
|
||||||
|
"de fenêtre UAC.",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
("🔌 Ports réseau", [
|
||||||
|
(
|
||||||
|
"UDP 51820 — sortant, poste client",
|
||||||
|
"Port par défaut du serveur WireGuard ; ajustable dans l'onglet "
|
||||||
|
"Tunnel & profils. C'est le seul flux que l'application émet "
|
||||||
|
"vers le serveur.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"UDP 51820 — entrant, côté serveur",
|
||||||
|
"Le serveur doit écouter dessus, et le routeur doit y rediriger "
|
||||||
|
"le trafic venant d'Internet. Sans cette redirection, le "
|
||||||
|
"handshake n'aboutit jamais et rien ne le signale : WireGuard "
|
||||||
|
"ignore silencieusement les paquets qu'il ne peut pas "
|
||||||
|
"authentifier.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Filtrage sur le port source",
|
||||||
|
"Un pare-feu ne doit pas exiger un port source particulier : un "
|
||||||
|
"client WireGuard émet depuis un port éphémère aléatoire. Le "
|
||||||
|
"diagnostic détecte ce cas.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"UDP/TCP 53 — DNS",
|
||||||
|
"Résolution du nom du serveur avant connexion, puis résolutions "
|
||||||
|
"dans le tunnel une fois monté.",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
("🔑 Privilèges", [
|
||||||
|
(
|
||||||
|
"Linux — root, sudo ou pkexec",
|
||||||
|
"Demandés commande par commande, au moment du besoin. Une règle "
|
||||||
|
"sudo évite la fenêtre d'authentification répétée ; le sous-onglet "
|
||||||
|
"<b>Sécurité → Droits sudo</b> indique lesquelles manquent.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Windows — administrateur",
|
||||||
|
"Pour l'installation et le retrait du service du tunnel.",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
("🐍 Depuis les sources", [
|
||||||
|
(
|
||||||
|
"Python 3.11 ou supérieur",
|
||||||
|
"Dépendances : PyQt6, cryptography, qrcode, Pillow, bcrypt. Les "
|
||||||
|
"binaires distribués les embarquent : rien à installer.",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ── Sécurité ─────────────────────────────────────────────────────────────
|
||||||
|
SECURITY = [
|
||||||
|
("Authentification", [
|
||||||
|
(
|
||||||
|
"Mot de passe administrateur haché avec bcrypt, coût 12",
|
||||||
|
"Le mot de passe n'est jamais stocké, même chiffré : seule son "
|
||||||
|
"empreinte l'est, avec un sel propre à chaque enregistrement.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Vérification résistante à l'analyse temporelle",
|
||||||
|
"<code>bcrypt.checkpw</code> compare l'empreinte sans laisser sa "
|
||||||
|
"durée trahir le nombre de caractères corrects. Les anciennes "
|
||||||
|
"empreintes SHA-256, encore acceptées le temps d'une migration, "
|
||||||
|
"passent par <code>secrets.compare_digest</code> pour la même "
|
||||||
|
"raison.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Code d'accès validé par le serveur",
|
||||||
|
"Le code à 6 chiffres part au serveur, seul détenteur du secret "
|
||||||
|
"et seul point où l'accès est réellement ouvert. L'ancien "
|
||||||
|
"vérificateur local ne verrouillait que sa propre fenêtre : le "
|
||||||
|
"secret vivait sur le poste, et le contourner suffisait. Un "
|
||||||
|
"secret hérité d'une version ≤ 0.8.0 est effacé au démarrage.",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
("Cryptographie", [
|
||||||
|
(
|
||||||
|
"Clés Curve25519 générées par la bibliothèque <code>cryptography</code>",
|
||||||
|
"Échange X25519, l'algorithme même de WireGuard. L'application ne "
|
||||||
|
"dépend pas du binaire <code>wg</code> pour produire ses clés.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Aléa fourni par le système d'exploitation",
|
||||||
|
"Clés pré-partagées et index de session tirés de "
|
||||||
|
"<code>os.urandom</code>, jamais d'un générateur pseudo-aléatoire "
|
||||||
|
"ordinaire.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Clé pré-partagée de 32 octets",
|
||||||
|
"Longueur imposée par WireGuard ; le format est vérifié avant "
|
||||||
|
"d'être accepté.",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
("Protection des secrets au repos", [
|
||||||
|
(
|
||||||
|
"Configuration du tunnel en 0600",
|
||||||
|
"Le fichier contenant la clé privée n'est lisible que par son "
|
||||||
|
"propriétaire, y compris le fichier temporaire employé pour "
|
||||||
|
"l'installer dans /etc/wireguard — créé directement en 0600, "
|
||||||
|
"jamais lisible par d'autres, même brièvement.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Clés privées masquées à l'écran",
|
||||||
|
"Affichées en points, révélées seulement sur demande explicite.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Export .conf également restreint",
|
||||||
|
"Un fichier exporté reçoit les mêmes permissions que la "
|
||||||
|
"configuration d'origine.",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
("Réduction de la surface d'attaque", [
|
||||||
|
(
|
||||||
|
"Privilèges demandés au coup par coup",
|
||||||
|
"L'application ne tourne pas en permanence avec les droits "
|
||||||
|
"d'administration : chaque commande privilégiée est élevée "
|
||||||
|
"séparément, par sudo puis pkexec.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Aucun appel passé par un interpréteur de commandes",
|
||||||
|
"Les commandes système sont invoquées sous forme de liste "
|
||||||
|
"d'arguments, ce qui exclut l'injection par le contenu d'un champ "
|
||||||
|
"de configuration.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Sonde de handshake sans état",
|
||||||
|
"Le test de connexion négocie une poignée de main puis jette "
|
||||||
|
"immédiatement la session : il ne monte aucun tunnel et ne "
|
||||||
|
"conserve aucun secret dérivé.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Nettoyage après un arrêt brutal",
|
||||||
|
"Interface orpheline et configuration DNS résiduelle sont "
|
||||||
|
"retirées, pour ne pas laisser le système dans un état "
|
||||||
|
"intermédiaire.",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def parse_changelog() -> list[dict]:
|
def parse_changelog() -> list[dict]:
|
||||||
"""Lit CHANGELOG.md : titres `## [x.y.z] — date`, sous-titres `### ...`
|
"""Lit CHANGELOG.md : titres `## [x.y.z] — date`, sous-titres `### ...`
|
||||||
et puces `- ...` (avec leurs lignes de continuation) uniquement — le
|
et puces `- ...` (avec leurs lignes de continuation) uniquement — le
|
||||||
|
|||||||
+59
-6
@@ -958,7 +958,7 @@ class AdminWindow(QDialog):
|
|||||||
# Onglet À propos
|
# Onglet À propos
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def _tab_about(self) -> QWidget:
|
def _tab_about(self) -> QWidget:
|
||||||
"""Page « À propos », en cinq sous-onglets (modèle QuiOulé/TuxMarket).
|
"""Page « À propos », en huit sous-onglets (modèle QuiOulé/TuxMarket).
|
||||||
|
|
||||||
Le contenu (fonctions, garanties, pile technique, auteur) vit dans
|
Le contenu (fonctions, garanties, pile technique, auteur) vit dans
|
||||||
`app/app_info.py`, séparé de sa présentation ici — le rendre
|
`app/app_info.py`, séparé de sa présentation ici — le rendre
|
||||||
@@ -974,11 +974,14 @@ class AdminWindow(QDialog):
|
|||||||
|
|
||||||
sub = QTabWidget()
|
sub = QTabWidget()
|
||||||
sub.setStyleSheet(theme.TABBAR_CSS)
|
sub.setStyleSheet(theme.TABBAR_CSS)
|
||||||
sub.addTab(self._subtab_about_features(app_info), "⭐ Fonctions")
|
sub.addTab(self._subtab_about_features(app_info), "⭐ Fonctions")
|
||||||
sub.addTab(self._subtab_about_guarantees(app_info), "🛡️ Garanties")
|
sub.addTab(self._subtab_about_howto(app_info), "📖 Mode d'emploi")
|
||||||
sub.addTab(self._subtab_about_tech(app_info), "🧱 Technique")
|
sub.addTab(self._subtab_about_requirements(app_info), "📦 Prérequis")
|
||||||
sub.addTab(self._subtab_about_changelog(app_info), "🕓 Journal")
|
sub.addTab(self._subtab_about_security(app_info), "🔐 Sécurité")
|
||||||
sub.addTab(self._subtab_about_author(app_info), "👤 Auteur")
|
sub.addTab(self._subtab_about_guarantees(app_info), "🛡️ Garanties")
|
||||||
|
sub.addTab(self._subtab_about_tech(app_info), "🧱 Technique")
|
||||||
|
sub.addTab(self._subtab_about_changelog(app_info), "🕓 Journal")
|
||||||
|
sub.addTab(self._subtab_about_author(app_info), "👤 Auteur")
|
||||||
lay.addWidget(sub)
|
lay.addWidget(sub)
|
||||||
return w
|
return w
|
||||||
|
|
||||||
@@ -1105,6 +1108,29 @@ class AdminWindow(QDialog):
|
|||||||
area.setWidget(inner)
|
area.setWidget(inner)
|
||||||
return area
|
return area
|
||||||
|
|
||||||
|
def _about_group_label(self, text: str) -> QLabel:
|
||||||
|
"""Intitulé séparant deux groupes de cartes dans une même liste.
|
||||||
|
|
||||||
|
Prérequis et Sécurité alignent une vingtaine de cartes chacun : sans
|
||||||
|
rien pour marquer où finit « Linux » et où commence « Windows », la
|
||||||
|
liste se lit comme un seul bloc indifférencié.
|
||||||
|
"""
|
||||||
|
lbl = QLabel(text)
|
||||||
|
lbl.setWordWrap(True)
|
||||||
|
lbl.setStyleSheet(
|
||||||
|
f"color: {theme.ACCENT_LIGHT}; font-size: 13px; font-weight: bold;"
|
||||||
|
" background: transparent; margin-top: 6px;"
|
||||||
|
)
|
||||||
|
return lbl
|
||||||
|
|
||||||
|
def _about_grouped(self, groups: list[tuple[str, list[tuple[str, str]]]]) -> QWidget:
|
||||||
|
"""Liste de cartes découpée en groupes titrés."""
|
||||||
|
items: list[QWidget] = []
|
||||||
|
for title, entries in groups:
|
||||||
|
items.append(self._about_group_label(title))
|
||||||
|
items += [self._about_card(body, title=head) for head, body in entries]
|
||||||
|
return self._about_card_list(items)
|
||||||
|
|
||||||
# ── Sous-onglet : fonctions ───────────────────────────────────────────
|
# ── Sous-onglet : fonctions ───────────────────────────────────────────
|
||||||
def _subtab_about_features(self, info) -> QWidget:
|
def _subtab_about_features(self, info) -> QWidget:
|
||||||
page, v = self._test_page()
|
page, v = self._test_page()
|
||||||
@@ -1112,6 +1138,33 @@ class AdminWindow(QDialog):
|
|||||||
v.addWidget(self._about_card_list(cards))
|
v.addWidget(self._about_card_list(cards))
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
# ── Sous-onglet : mode d'emploi ───────────────────────────────────────
|
||||||
|
def _subtab_about_howto(self, info) -> QWidget:
|
||||||
|
page, v = self._test_page()
|
||||||
|
v.addWidget(self._hint(info.HOWTO_FIRST_INTRO))
|
||||||
|
items: list[QWidget] = [
|
||||||
|
self._about_card(step, icon=f"{i}\u20e3")
|
||||||
|
for i, step in enumerate(info.HOWTO_FIRST, 1)
|
||||||
|
]
|
||||||
|
items.append(self._about_group_label("Au quotidien"))
|
||||||
|
items += [self._about_card(body, title=head) for head, body in info.HOWTO_DAILY]
|
||||||
|
items.append(self._about_group_label("Quand la connexion échoue"))
|
||||||
|
items += [self._about_card(body, title=head) for head, body in info.HOWTO_TROUBLE]
|
||||||
|
v.addWidget(self._about_card_list(items))
|
||||||
|
return page
|
||||||
|
|
||||||
|
# ── Sous-onglet : prérequis ───────────────────────────────────────────
|
||||||
|
def _subtab_about_requirements(self, info) -> QWidget:
|
||||||
|
page, v = self._test_page()
|
||||||
|
v.addWidget(self._about_grouped(info.REQUIREMENTS))
|
||||||
|
return page
|
||||||
|
|
||||||
|
# ── Sous-onglet : sécurité ────────────────────────────────────────────
|
||||||
|
def _subtab_about_security(self, info) -> QWidget:
|
||||||
|
page, v = self._test_page()
|
||||||
|
v.addWidget(self._about_grouped(info.SECURITY))
|
||||||
|
return page
|
||||||
|
|
||||||
# ── Sous-onglet : garanties ───────────────────────────────────────────
|
# ── Sous-onglet : garanties ───────────────────────────────────────────
|
||||||
def _subtab_about_guarantees(self, info) -> QWidget:
|
def _subtab_about_guarantees(self, info) -> QWidget:
|
||||||
page, v = self._test_page()
|
page, v = self._test_page()
|
||||||
|
|||||||
Reference in New Issue
Block a user