Systeme d'alarme IoT evenementiel : hexagonal, CQRS, Kafka

Backend d'un systeme de video-surveillance dont le capteur est un iPhone
recycle. Le probleme est simple, la structure est le sujet :

- domain/ sans aucun import de framework (verifie par 7 regles ArchUnit
  qui tournent aussi pendant le docker build)
- CQRS strict : write et read ne partagent ni modele ni port
- AlarmDecision en sealed interface -> switch exhaustif verifie a la
  compilation, plutot qu'une chaine de if
- Kafka en KRaft (Zookeeper est supprime depuis Kafka 4.0), cle de
  partition = deviceId pour garantir l'ordre par capteur
- ErrorHandlingDeserializer + DLQ : un message malforme ne bloque pas
  le flux des evenements suivants
- notification WebSocket differee apres commit, pour ne jamais afficher
  une intrusion qui partirait en rollback

31 tests au vert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-09-12 17:32:45 +02:00
commit 63ead8a966
81 changed files with 5051 additions and 0 deletions

39
deploy/Caddyfile.snippet Normal file
View file

@ -0,0 +1,39 @@
# ---------------------------------------------------------------------------
# Bloc a ajouter a /opt/edge/Caddyfile sur le serveur Hetzner.
#
# Pourquoi Caddy et pas Nginx + Certbot : le serveur heberge deja cinq autres
# domaines derriere un Caddy qui detient les ports 80 et 443. Y demarrer un
# second frontal aurait mis les deux en conflit sur les memes ports et coupe
# les services existants. Caddy obtient et renouvelle les certificats
# Let's Encrypt tout seul - il n'y a pas de Certbot a installer.
#
# Les deux orthographes sont servies : "seurveillance" est celle du sous-domaine
# demande, "surveillance" couvre la faute de frappe dans les deux sens.
# Caddy demande un certificat pour chaque nom qui resout vers ce serveur.
#
# Le proxy WebSocket n'a rien a declarer : Caddy propage l'upgrade nativement.
# ---------------------------------------------------------------------------
video-seurveillance.belkacemi.me, video-surveillance.belkacemi.me {
reverse_proxy iot-alarm-app:8080
# L'actuator n'a aucune raison d'etre public : health et metriques
# renseignent un attaquant sur les versions et l'etat interne.
@actuator path /actuator/*
respond @actuator 404
encode gzip zstd
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
log {
output file /var/log/caddy/iot-alarm.log
format json
}
}

54
deploy/nginx/nginx.conf Normal file
View file

@ -0,0 +1,54 @@
# Reverse proxy pour l'execution en local (profil "standalone").
# En production sur Hetzner, ce role est tenu par le Caddy deja en place.
upstream iot_alarm_app {
server app:8080;
}
server {
listen 80;
server_name _;
access_log /var/log/nginx/iot-alarm.access.log;
error_log /var/log/nginx/iot-alarm.error.log warn;
# --- WebSocket / SockJS -------------------------------------------------
# Sans l'upgrade explicite, la negociation STOMP echoue silencieusement et
# la console retombe indefiniment sur "reconnexion...".
location /ws {
proxy_pass http://iot_alarm_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Un websocket inactif ne doit pas etre coupe au bout de 60 s :
# la console reste ouverte des heures sans qu'il se passe rien.
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
}
# --- Application --------------------------------------------------------
location / {
proxy_pass http://iot_alarm_app;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
# --- Supervision : non expose ------------------------------------------
location /actuator/ {
allow 127.0.0.1;
allow 172.16.0.0/12;
deny all;
proxy_pass http://iot_alarm_app;
proxy_set_header Host $host;
}
}