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>
4.6 KiB
Architecture
Overview
Stupid Simple Network Inventory is a self-hosted web application for manual network inventory and logical topology visualisation. There is no auto-discovery of topology — only ICMP reachability scanning. All data is entered manually.
Request Flow
Browser
│
│ HTTP :8080
▼
┌─────────────────────────────────────┐
│ Nginx (frontend container) │
│ serves /usr/share/nginx/html │
│ │
│ location /api/ → proxy_pass │
│ http://backend:8000/api/ │
└──────────────────┬──────────────────┘
│ HTTP :8000 (Docker internal network)
▼
┌─────────────────────────────────────┐
│ FastAPI (backend container) │
│ uvicorn 0.0.0.0:8000 │
│ │
│ /app/data/ (bind mount) │
│ topology.db ← SQLite │
│ secret_key.txt ← JWT secret │
└─────────────────────────────────────┘
Docker Compose
Two services defined in docker-compose.yml:
| Resource | Type | Notes |
|---|---|---|
backend |
service | Python 3.11-slim, runs as DOCKER_UID:DOCKER_GID (host user), cap_drop: ALL, cap_add: NET_RAW |
frontend |
service | Multi-stage (Vite → nginxinc/nginx-unprivileged), UID 101, cap_drop: ALL, no-new-privileges, exposes :8080 |
./db_data:/app/data is a bind mount. The backend process runs with the same UID/GID as the host user owning ./db_data/ (set via DOCKER_UID/DOCKER_GID in .env). Pre-create the directory before the first run: mkdir -p db_data.
Both containers share an internal Docker bridge network (internal). The browser never communicates directly with the backend — all API traffic goes through Nginx.
Build Pipeline
Backend (backend/Dockerfile):
python:3.11-slimbase- Install
iputils-ping(required for ICMP subprocess calls) pip install -r requirements.txt- Start with
uvicorn main:app --host 0.0.0.0 --port 8000
Frontend (frontend/Dockerfile):
- Stage 1:
node:20-alpine— runsvite build, outputs to/app/dist - Stage 2:
nginx:alpine— copies/app/distto/usr/share/nginx/html, copiesnginx.conf
Startup Sequence (backend)
main.py runs synchronously at import time before the ASGI app is created:
_migrate_vlan_nullable()— idempotent DDL fix for vlans table_migrate_device_virt_type()— adds column if missing_migrate_device_url()— adds column if missing_migrate_users()— creates users table + seeds admin accountBase.metadata.create_all(bind=engine)— creates any missing tables- FastAPI app instance created, routers registered
This ordering ensures migrations never run against an uninitialised schema.
Authentication Architecture
LoginPage.vue
│ POST /api/auth/login (form-urlencoded)
│ ← { access_token, token_type, username, must_change_password }
│
▼
auth.js (setAuth)
stores token + username + mustChangePassword in localStorage
─────────────────────────────────────────────────────────────
isAuthenticated (computed ref) ← App.vue reads this
mustChangePassword (computed ref) ← App.vue shows AccountModal :forced when true
getToken() ← api.js reads this per request
App.vue template guard:
!isAuthenticated → <LoginPage>
mustChangePassword → <AccountModal :forced="true"> (blocks the app)
else → full application
api.js (axios interceptor)
every request → Authorization: Bearer <token>
every 401 response (if token existed) → clearAuth() + reload
The JWT payload is { sub: username, ver: token_version, exp: <24h> }, signed with HS256.
The secret is loaded from data/secret_key.txt (auto-generated on first run, permissions 0600) or SECRET_KEY env var.
Password change invalidates previous tokens immediately via token_version bump.
Persistence
All state is in SQLite at ./db_data/topology.db. There is no caching layer, no background jobs, no message queue. The only writable files at runtime are topology.db and secret_key.txt, both in the bind-mounted ./db_data/ directory on the host.