# 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` 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` → `` - `mustChangePassword` → `` (bloque l'application) - sinon → application complète Template enveloppé dans `
` (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 `