Files
stupid-simple-network-inven…/CLAUDE.md
T
olivier 88cf6458d0 Initial commit — Stupid Simple Network Inventory
Application web d'inventaire réseau manuel avec FastAPI, Vue 3 et Docker.
Inclut l'authentification JWT, la découverte ICMP, et la topologie en cards CSS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 09:19:19 +02:00

248 lines
14 KiB
Markdown

# Stupid Simple Network Inventory
Application web d'inventaire manuel de serveurs et de génération de topologie réseau logique.
## Stack technique
- **Backend** : FastAPI (Python 3.11) + SQLAlchemy + SQLite
- **Frontend** : Vue 3 + Vite (pas de Cytoscape — topologie en cards CSS)
- **Icônes** : `lucide-vue-next` (UI générique) + `simple-icons` (logos de marques)
- **Reverse proxy** : Nginx (sert le frontend et proxifie `/api/` vers le backend)
- **Conteneurisation** : Docker Compose
## Lancement
```bash
docker compose up --build -d
```
L'application est accessible sur **http://localhost:8080**
## Architecture
```
topologie/
├── docker-compose.yml
├── backend/
│ ├── Dockerfile # Python 3.11-slim + iputils-ping (pour ICMP)
│ ├── requirements.txt # fastapi, sqlalchemy, dnspython, ...
│ ├── main.py # Point d'entrée FastAPI + migrations SQLite au démarrage
│ ├── database.py # SQLAlchemy engine + session
│ ├── models.py # Modèles ORM (Vlan, Device, DeviceInterface)
│ └── routers/
│ ├── vlans.py
│ ├── devices.py
│ └── discovery.py # POST /api/discovery/scan + POST /api/discovery/ping
└── frontend/
├── Dockerfile # Multi-stage: build Vite → Nginx
├── nginx.conf # Proxy /api/ → backend:8000
├── index.html
├── package.json
├── vite.config.js
└── src/
├── main.js
├── App.vue # Layout + état global (vlans, devices) + garde auth
├── api.js # Appels axios (vlansApi, devicesApi, discoveryApi, authApi)
├── auth.js # État auth : isAuthenticated, currentUsername, setAuth(), clearAuth()
├── i18n.js # Internationalisation : locale (ref), t(key), tFmt(key, ...args), setLocale()
├── theme.js # Thème : theme (ref), toggleTheme() — classe html.dark
├── brandIcons.js # Tableau BRANDS + fonction detectBrands() — partagé entre composants
└── components/
├── TopologyGraph.vue # Vue cards par réseau + ping statut
├── DeviceIcon.vue # Icône Lucide par type (prop typeOnly) ou logos marques
├── DeviceManager.vue # CRUD équipements + barre filtres/recherche
├── VlanManager.vue # CRUD réseaux (VLANs + LANs classiques)
├── DiscoveryModal.vue # Découverte auto : ping sweep + DNS PTR
├── LoginPage.vue # Page de connexion plein écran
└── AccountModal.vue # Modale changement nom d'utilisateur / mot de passe
```
## Modèle de données
- **User** : id, username (unique), hashed_password (bcrypt)
- **Vlan** : id, vlan_id (int, nullable — null = LAN classique sans tag 802.1Q), name, cidr, color
- **Device** : id, name, type, description, is_gateway, is_livebox, virt_type (nullable), url (nullable)
- **DeviceInterface** : id, device_id, vlan_id (nullable), ip_address, name, is_upstream
## Types d'équipements (18)
`server`, `switch`, `router`, `nas`, `gateway`, `livebox`, `access_point`, `camera`, `temperature`, `sensor`, `hub`, `smart_plug`, `alarm`, `light`, `doorbell`, `desktop`, `laptop`, `other`
## Champ virt_type (Device)
Libellé dans le formulaire : "Type d'environnement d'exécution". Valeurs :
- `null` — non précisé (défaut)
- `baremetal` — serveur physique
- `lxc` — conteneur LXC (tag bleu "LXC" dans la topologie)
- `qemu` — VM QEMU/KVM (tag violet "VM" dans la topologie)
## Champ url (Device)
URL optionnelle d'accès à l'interface web de l'équipement. Non applicable aux types `desktop` et `laptop` (masqué dans le formulaire). Affiché comme lien cliquable dans la card équipement et comme tag "Link" vert sur les chips de topologie.
## Réseaux (VlanManager.vue)
`vlan_id` est optionnel. Si absent → réseau LAN classique (badge "LAN"). Si présent → VLAN 802.1Q (badge "VLAN X").
Les réseaux sont triés : LANs en premier (vlan_id NULL), puis VLANs par ID croissant.
## Topologie (TopologyGraph.vue)
Pas de graphe Cytoscape. Layout en cards CSS :
- **Barre d'outils** en haut : bouton Ping avec indicateur animé + compteur up/down
- **Carte WAN** (rouge) — devices `is_livebox`
- **Carte Passerelle** (jaune) — devices `is_gateway`
- **Grille réseaux** — une card par réseau (LAN ou VLAN), chaque device apparaît dans toutes les cards de ses réseaux (multi-réseau natif)
- **Zone non assigné** — devices sans interface réseau, ni livebox, ni gateway
- **Tri** : devices triés par IP croissante dans chaque card
### Layout des cards
- **Cards pleine largeur**, empilées verticalement (flex-col) — toutes de la même largeur, hauteur variable selon le nombre de devices
- Barre colorée en haut de chaque card (3px, couleur du réseau), bordures latérales neutres
- Header : badge VLAN/LAN (pill colorée) + nom + CIDR inline + compteur de devices (pill)
- **Chips en grille horizontale** (`grid auto-fill, minmax(210px, 1fr)`) à l'intérieur de chaque card — s'adaptent automatiquement au nombre de colonnes disponibles
- Responsive : sous 600px, une seule colonne de chips
### Device chip
- Icône de type (Lucide) toujours dans le carré arrondi (28px) avec fond coloré
- Nom (bold 12px) + IP (pill monospace) + nom d'interface (grisé) + logos de marques (SVG colorés 11px)
- Tags à droite en colonne : **Link** (vert, cliquable — tag URL de l'équipement) / **GW** / **LXC** / **VM**
- Dot de ping : vert (joignable) ou rouge (injoignable), affiché après un ping
## Icônes (DeviceIcon.vue + brandIcons.js)
La détection de marque est centralisée dans `brandIcons.js` (`detectBrands(name, description)`).
`DeviceIcon.vue` accepte une prop `typeOnly` (booléen) :
- `false` (défaut) : affiche les logos de marques si détectés, sinon icône Lucide — utilisé dans TopologyGraph
- `true` : toujours l'icône Lucide par type — utilisé dans DeviceManager
Dans **DeviceManager**, les logos de marques apparaissent comme badges colorés séparés (couleur officielle de la marque) dans les meta de la card.
Dans **TopologyGraph**, les logos de marques apparaissent comme petits SVG 11px colorés dans `chip-sub`.
Marques supportées via Simple Icons :
Proxmox, Docker, Synology, TrueNAS, Ubiquiti/UniFi, MikroTik, Cisco, TP-Link, ASUS, Netgear, pfSense, OPNsense, OpenWrt, Apache/Apache2, Traefik, MariaDB, Kubernetes/k3s, Debian, Ubuntu, Ansible, Dell, HP, Raspberry Pi, Arduino, KDE/Plasma, Excalidraw, Nextcloud, Paperless-NGX, Uptime Kuma, MkDocs, Jellyfin, Home Assistant, Philips Hue, Xiaomi
Pour ajouter une marque : vérifier avec `node -e "const si = require('./node_modules/simple-icons'); console.log(Object.keys(si).filter(k => k.toLowerCase().includes('nom')))"`, importer `si<Name>` dans `brandIcons.js`, ajouter une entrée dans BRANDS.
Fallback : Lucide (icône par type d'équipement).
## Vue Équipements (DeviceManager.vue)
### Barre de filtres + recherche
Barre compacte (28px) combinant :
- **Champ de recherche** : filtre par nom, description ou IP en temps réel (Échap pour vider)
- **Filtre Type** : dropdown multi-sélection, seuls les types présents dans les données
- **Filtre Réseau** : dropdown avec pastille colorée par VLAN/LAN
- **Filtre Marque** : dropdown avec icône SVG, trié alphabétiquement
- **Filtre Virt** : dropdown, seulement si au moins un équipement a un virt_type
Tous les filtres sont combinables (ET entre catégories, OU à l'intérieur). Bouton "Effacer" + compteur `X / total` si filtre actif.
## Ping (discovery.py + TopologyGraph.vue)
`POST /api/discovery/ping` — ping ICMP parallèle (50 workers).
- Body : `{ ips: ["1.2.3.4", ...] }`
- Retourne : `[{ ip, alive }, ...]`
Dans la topologie : bouton "Ping" déclenche le ping de toutes les IPs connues. Un équipement est "up" si au moins une de ses IPs répond. Dot vert/rouge sur chaque chip.
## Authentification (auth.py + auth.js)
JWT HS256, **expiration 24h**. Payload : `{ sub: username, ver: token_version, exp: ... }`. Clé secrète dans `data/secret_key.txt` (permissions 0600, auto-générée) ou `SECRET_KEY` env var.
**Compte par défaut** : `admin` / `admin` avec `must_change_password=1` — créé si la table `users` est vide. Définir `INITIAL_ADMIN_PASSWORD` pour un mot de passe personnalisé (sans changement forcé). La migration `_migrate_force_admin_password_change()` remet `must_change_password=1` si l'admin utilise encore le mot de passe bootstrap sur une base existante.
**Invalidation de token** : le changement de mot de passe incrémente `User.token_version`. `get_current_user` rejette tout token dont `ver` ne correspond pas → invalidation immédiate des sessions précédentes.
**Compatibilité bcrypt** : `passlib 1.7.4` est incompatible avec `bcrypt >= 4.0`. Le fichier `requirements.txt` épingle `bcrypt==3.2.2`. Ne pas mettre à jour `bcrypt` sans mettre à jour `passlib`.
### Endpoints auth
| Méthode | Chemin | Auth requise | Description |
|---------|--------|:------------:|-------------|
| POST | `/api/auth/login` | Non | Form → `{ access_token, token_type, username, must_change_password }` — 429 si rate-limited |
| PUT | `/api/auth/account` | Oui (même si must_change) | `{ current_password, new_username?, new_password? }` → nouveau token + `must_change_password` |
| GET | `/api/auth/me` | Oui | `{ username, must_change_password }` |
Routeurs métier protégés via `dependencies=[Depends(require_password_changed)]` (= `get_current_user` + rejet 403 si `must_change_password=True`).
### Côté frontend
`auth.js` expose `isAuthenticated`, `currentUsername`, `mustChangePassword` (refs) et `setAuth(token, username, mustChange)`, `clearAuth()`, `getToken()`. Persisté dans `localStorage`.
`App.vue` guard :
- `!isAuthenticated``<LoginPage>`
- `mustChangePassword``<AccountModal :forced="true">` (bloque l'application)
- sinon → application complète
Template enveloppé dans `<div class="app-root">` (un seul nœud racine) pour éviter le bug Firefox avec l'overlay d'autofill sur les fragments Vue (plusieurs nœuds racines). `.app-root` utilise `display: flex; flex-direction: column; min-height: 100vh` — ne pas revenir à `display: contents` (bug Firefox : les clics ne sont plus dispatché aux enfants après un remplacement du DOM à l'intérieur d'un élément `display: contents`).
## Migrations SQLite (main.py)
`main.py` exécute des migrations idempotentes au démarrage avant `create_all` :
- `_migrate_vlan_nullable()` — recrée `vlans` sans contrainte NOT NULL sur `vlan_id`
- `_migrate_device_virt_type()` — ajoute `virt_type` si absente
- `_migrate_device_url()` — ajoute `url` si absente
- `_migrate_users_must_change_password()` — ajoute `must_change_password` si absente
- `_migrate_users_token_version()` — ajoute `token_version` si absente
- `_migrate_force_admin_password_change()` — rattrapage : force must_change si admin utilise le mot de passe bootstrap
- `_migrate_drop_links_table()` — supprime la table `links` (fonctionnalité retirée en phase 3)
- `_migrate_users()` — crée `users` + compte admin si vide
Pour toute nouvelle colonne nullable sur une table existante, ajouter une fonction de migration du même type.
Quand on ajoute un champ à Device : mettre à jour `models.py`, ajouter la migration dans `main.py`, ajouter le champ dans `DeviceCreate` et `DeviceOut` dans `routers/devices.py`, l'assigner explicitement dans `create_device` et `update_device`.
## Découverte automatique (DiscoveryModal.vue)
`POST /api/discovery/scan` — ping sweep ICMP + lookup PTR DNS contre un serveur DNS configurable.
- Max 1024 hôtes par VLAN, 100 workers concurrents
- Retourne : ip, hostname, vlan_id, durée
- Nécessite `cap_add: NET_RAW` sur le service backend (déjà configuré)
## Thème (theme.js + App.vue)
`frontend/src/theme.js` exporte `theme` (ref) et `toggleTheme()`.
- Valeurs : `'light'` (défaut) / `'dark'`
- Persisté dans `localStorage('theme')`
- Applique / retire la classe `html.dark` via `watch`
Les variables CSS sont définies dans le `<style>` global de `App.vue` :
- `:root` — thème clair (`--bg-page: #F1F5F9`, `--bg-card: #ffffff`, `--text-primary: #0F172A`, ...)
- `html.dark` — thème sombre (`--bg-page: #0F172A`, `--bg-card: #1E293B`, `--text-primary: #F1F5F9`, ...)
Variables disponibles : `--bg-page`, `--bg-card`, `--bg-card-hover`, `--bg-input`, `--bg-thead`, `--bg-chip`, `--bg-chip-hover`, `--border`, `--border-strong`, `--text-primary`, `--text-secondary`, `--text-muted`, `--text-faint`, `--chip-ip-bg`, `--chip-ip-color`, `--modal-overlay`, `--shadow-card`, `--shadow-modal`, `--shadow-drop`.
Bouton toggle (☾/☀) dans le footer de la sidebar.
**Règle** : toutes les couleurs de fond/texte/bordure dans les composants doivent utiliser ces variables, pas des valeurs hex hardcodées.
Exception : les couleurs sémantiques fixes (couleur des icônes par type d'équipement dans les chips, couleurs des badges VLAN) restent en hex car elles ne dépendent pas du thème.
## Internationalisation (i18n.js)
`frontend/src/i18n.js` exporte :
- `locale` (ref) — langue active, persistée dans `localStorage('locale')`, défaut `'fr'`
- `setLocale(lang)` — change la langue
- `t(key)` — retourne la traduction, fallback `'fr'` puis la clé brute
- `tFmt(key, ...args)` — idem avec interpolation `{0}`, `{1}`, ...
Langues supportées : **français** (`fr`), **anglais** (`en`), **espagnol** (`es`).
Sélecteur (pills `fr / en / es`) dans le footer de la sidebar.
**Règle** : toutes les chaînes visibles dans l'UI passent par `t('key')` — jamais de texte hardcodé dans les templates. `deviceTypes` dans `DeviceManager` est un `computed` (pas une constante) pour réagir aux changements de locale.
## Favicon
`frontend/public/favicon.svg` — SVG indigo (#6366f1), même forme que le logo sidebar (graphe de nœuds réseau).
Référencé dans `frontend/index.html` : `<link rel="icon" type="image/svg+xml" href="/favicon.svg" />`.
## Données persistées
Volume Docker `db_data` monté sur `/app/data/``topology.db` (SQLite)