Contrat API — Bascule d’un client vers J+RADIUS
Le bouton « migrer ce client en mode RADIUS », et ce que le gérant voit pendant que ça se passe.
📱 Dev de l’application : le briefing d’intégration pas à pas — écrans, pièges, protocole de test des 7 routes — est sur
/docs/CONTRAT-API-MIGRATION-RADIUS-APP. Ce document-ci reste la référence des champs.
Base : https://live.jmoai.net/api/v1/radius
1. Le fait qui rend cette bascule nécessaire
[!WARNING] Sur RouterOS, le hotspot consulte d’abord sa base locale et n’interroge RADIUS que si l’usager n’y figure pas.
Poser use-radius=yes en laissant les comptes locaux ne bascule donc rien : les clients
continuent de s’authentifier en local, RADIUS n’est jamais appelé, et aucune erreur n’est
levée.
Mesuré le 2026-08-29 : 8 707 comptes locaux sur le routeur de référence, et radacct à
874 lignes pour tout le parc, dont zéro authentification hotspot. Des déploiements
avaient pourtant réussi. La bascule était inerte.
Le retrait des comptes locaux n’est donc pas un nettoyage de confort : c’est l’étape qui fait la bascule.
Le temps déjà consommé n’est pas perdu pendant l’import : l’uptime lu sur le MikroTik est
déduit de Max-All-Session avant l’écriture RADIUS. Exemple : un voucher de 2 h avec 1 h déjà
utilisée est publié avec 1 h restante. Limite importante : cette règle prépare la prochaine
authentification RADIUS ; une session déjà ouverte en local reste pilotée par le routeur jusqu’à
sa reconnexion, son expiration locale ou une coupure contrôlée.
2. Les six étapes, dont une seule est irréversible
1. inventaire on regarde avant de promettre
2. sauvegarde rapatriée ET vérifiée, sinon on s'arrête
3. import vers RADIUS et on COMPTE ce qui est arrivé
4. configuration le routeur parle au serveur, la confirmation est armée
5. retrait du local ← IRRÉVERSIBLE, exige trois preuves
6. première connexion une vraie authentification, avec le nom de l'usager
Les étapes 1 à 4 se rejouent sans dommage. La 5 exige trois preuves :
| Preuve | Comment elle est obtenue |
|---|---|
SAUVEGARDE_CONFIRMEE |
des octets rapatriés, pas un simple retour sans exception |
IMPORT_RADIUS_CONFIRME |
inventaire paginé du serveur RADIUS, complete: true |
ROUTEUR_JOIGNABLE |
l’inventaire a répondu |
Et un import incomplet bloque le retrait : des comptes non importés puis retirés, ce sont des tickets vendus qui disparaissent.
Si le retrait est refusé, la bascule s’arrête proprement : le routeur reste en double (local + RADIUS), le gérant continue de vendre, personne ne perd rien.
3. GET /admin/migration/{allocationId}/inventaire
Lecture seule. Aucune écriture, aucun engagement.
{
"allocation_id": "uuid",
"nas_id": "uuid",
"routeur": "HH40AF7YVNP",
"licence_active": true,
"tunnel_joignable": true,
"comptes_locaux": 8707,
"espace_flash_mo": 36,
"autorise": true,
"motif": null,
"motif_texte": null,
"avertissements": []
}
motif |
Sens |
|---|---|
LICENCE_RADIUS_INACTIVE |
pas de licence J+RADIUS active |
ROUTEUR_INJOIGNABLE |
le routeur ne répond pas |
INVENTAIRE_ILLISIBLE |
comptes non lisibles — ce n’est pas zéro compte |
TROP_DE_COMPTES_POUR_UNE_BASCULE_AUTOMATIQUE |
plus de 15 000 : lot par lot, pas en un clic |
ESPACE_FLASH_INSUFFISANT |
moins de 25 Mo : la sauvegarde échouerait |
comptes_locaux: null n’est pas 0. Le serveur refuse dans ce cas. Une application qui
afficherait « rien à migrer » sur un inventaire illisible enverrait un administrateur basculer
à l’aveugle un routeur qui porte huit mille comptes.
4. POST /admin/migration/{allocationId}
{ "retirer_local": true }
retirer_local: false fait tout sauf le retrait : utile pour préparer une migration, ou
laisser un routeur en double le temps d’observer.
{
"success": true,
"allocation_id": "uuid",
"nas_id": "uuid",
"statut": "ARMEMENT",
"etapes": [
{ "etape": "SAUVEGARDE", "etat": "OK", "comptes": 8707 },
{ "etape": "CONFIG", "etat": "OK" },
{ "etape": "IMPORT", "etat": "OK", "importes": 8707 },
{
"etape": "VERIFICATION",
"etat": "PARTIEL",
"par_etat": { "PRET": 6412, "EPUISE_LOCAL": 852, "ABSENT_VOUCHER": 3 },
"importes": 1829,
"realignes": 374,
"bloquants": 3,
"retirables": 7210
},
{
"etape": "RETRAIT_LOCAL",
"etat": "PARTIEL",
"retires": 7210,
"restant_local": 42,
"bloquants": 3
},
{ "etape": "ARMEMENT", "etat": "OK" }
],
"retires": 8706,
"importes": 8707
}
RETRAIT_LOCAL peut valoir REFUSE avec un motif — la bascule s’est arrêtée là, et c’est
un succès partiel, pas un échec : rien n’a été détruit.
Vérification compte par compte (depuis le 2026-09-22). Avant tout retrait, chaque compte
local est confronté à son voucher RADIUS et à FreeRADIUS. Les comptes absents sont importés, le
temps que RADIUS offrait en trop (import antérieur à la consommation) est ramené au reste local,
puis tout est relu. Ne sont retirés que les comptes prouvés (PRET ou EPUISE_LOCAL), hors
session en cours et sans temps en trop, par identifiant routeur et par lots recomptés. Les autres
restent en local et continuent de fonctionner : PARTIEL signale qu’il en reste. Rejouer la
bascule retire ceux devenus prouvés entre-temps. VERIFICATION en ECHEC arrête avant tout
retrait.
États bloquants : ABSENT_VOUCHER, ABSENT_FREERADIUS, MOT_DE_PASSE_DIFFERENT,
MOT_DE_PASSE_ILLISIBLE, VOUCHER_INUTILISABLE, TEMPS_RABOTE,
DUREE_ABSENTE_RADIUS (le routeur limite la durée, RADIUS non). Tout écart de temps, dans un
sens ou dans l’autre, est réaligné sur le reste local avant le retrait. PROFIL_DIFFERENT et
DESACTIVE_LOCAL ne bloquent pas mais ne sont jamais retirés automatiquement.
Le compte default-trial est toujours épargné : il est créé par RouterOS et son retrait
casse l’essai gratuit du hotspot.
| Code d’erreur | Sens |
|---|---|
MIGRATION_BACKUP_FAILED / MIGRATION_BACKUP_EMPTY |
sauvegarde impossible ou vide — rien n’a été touché |
MIGRATION_DEPLOY_FAILED |
configuration RADIUS refusée — rien n’a été supprimé |
MIGRATION_COUNT_FAILED |
comptage impossible — retrait annulé |
MIGRATION_REMOVE_STALLED |
le routeur ne retire plus rien — des comptes subsistent |
MIGRATION_VERIFY_INCOMPLETE |
inventaire FreeRADIUS incomplet — aucun retrait |
Le retrait est vérifié par comptage avant/après, pas par le retour de la commande : un retrait partiel qui passerait pour complet laisserait des comptes locaux, et ce sont eux qui masquent RADIUS.
4 bis. POST /admin/migration/{allocationId}/retour — revenir en local
Le bouton inverse. Remet les comptes hotspot sur le routeur, puis coupe RADIUS.
{ "couper_radius": true }
couper_radius: false remet les comptes sans couper RADIUS : le routeur reste en double le
temps de vérifier que tout répond avant de trancher.
Roaming MAC
POST /vouchers/:voucherId/bind-mac lie un MAC au voucher du même client. Le serveur RADIUS
ne crée pas de compte MAC : il résout la liaison vers le voucher encore valide et vérifie le
tenant du NAS. La liaison est retirée quand le voucher est supprimé, suspendu ou épuisé.
L’API ne modifie jamais login-by sur un routeur à cette occasion. Le MAC-login est un opt-in
explicite du propriétaire du NAS ; sans cet opt-in, les comptes et l’authentification existants
restent inchangés.
Reconnexion douce d’une session locale
POST /hotspot/handoff-local prépare une session Hotspot locale déjà ouverte pour la bascule
vers le voucher RADIUS correspondant. Le serveur lit la session active sur le MikroTik, récupère
son MAC, lie ce MAC au voucher, déduit l’uptime local du temps restant, puis peut couper la
session locale si cut_local: true est demandé.
{
"allocation_id": "uuid",
"username": "TM-E5DCDE",
"cut_local": false
}
La coupure est acceptée uniquement si le profil /ip/hotspot/profile du serveur actif contient
mac-cookie ou mac dans login-by. Sinon la réponse est 409 HANDOFF_AUTO_RECONNECT_NOT_READY
et aucune session locale n’est supprimée. mac-cookie favorise le retour sur le même routeur;
mac permet le MAC-login RADIUS si le format MAC du NAS et le profil sont configurés pour cela.
La reconnexion quasi immédiate est donc une possibilité vérifiée, jamais une garantie aveugle:
le client peut encore subir une brève nouvelle authentification selon son appareil, son cookie et
le réseau.
Réponses et refus importants:
| Code | Sens |
|---|---|
PRET_RECONNEXION_DOUCE |
MAC liée, temps restant calculé, aucune coupure demandée |
SESSION_LOCALE_COUPÉE_RECONNEXION_ATTENDUE |
session locale supprimée après contrôle du profil |
LOCAL_HOTSPOT_SESSION_NOT_FOUND |
aucune session locale correspondant au username |
LOCAL_SESSION_MAC_MISSING |
la session active ne fournit pas de MAC exploitable |
RADIUS_VOUCHER_NOT_RECONNECTABLE |
voucher supprimé, désactivé ou épuisé |
HANDOFF_AUTO_RECONNECT_NOT_READY |
login-by ne contient ni mac-cookie ni mac |
Le service ne modifie pas login-by, ne crée pas de compte Hotspot local et ne prétend pas que
la session est déjà authentifiée par RADIUS. Il prépare et vérifie uniquement le passage. Le
temps déjà consommé sur la session locale est conservé dans le calcul du temps restant.
{
"success": true,
"statut": "REVENU_EN_LOCAL",
"comptes_restaures": 8706,
"sauvegarde_datee_du": "2026-08-29T20:00:00Z",
"ecart_avec_radius": {
"connu": true,
"ecart": 53,
"avertissement": "Le serveur cloud porte 53 compte(s) de plus que la sauvegarde : ce sont des tickets vendus depuis la bascule. Ils ne seront PAS restaurés sur le routeur."
},
"etapes": [
{ "etape": "LECTURE_SAUVEGARDE", "etat": "OK", "comptes": 8707, "a_restaurer": 8706 },
{ "etape": "PROFILS", "etat": "OK", "profils": 18 },
{ "etape": "COMPTES", "etat": "OK", "poses": 8706 },
{ "etape": "VERIFICATION", "etat": "OK", "comptes": 8706 },
{ "etape": "RADIUS_COUPE", "etat": "OK" }
]
}
Ce qui n’est JAMAIS rejoué
[!WARNING] Le fichier
.backupbinaire n’est jamais restauré. Il porte toute la configuration — pare-feu, adresses, routes, peers WireGuard, tunnel de gestion. Le rejouer à distance rendrait le routeur muet une fois sur deux, et on perdrait le chemin d’accès en même temps que la possibilité de réparer.
Seuls les profils et les comptes hotspot sont remis. Rien d’autre n’est touché.
L’ordre, et pourquoi il ne s’inverse pas
- Profils d’abord — un compte dont le profil manque échoue, et fait tomber son lot entier.
- Comptes par lots de 40, commandes idempotentes : rejouer après un échec partiel ne lève pas « already have such name ».
- Vérification par comptage.
use-radius=noen dernier. Couper RADIUS avant d’avoir remis les comptes laisserait le hotspot sans aucune base d’authentification — tous les clients dehors pendant la restauration. Si la restauration échoue ou reste incomplète, RADIUS n’est pas coupé : le hotspot garde une base qui répond.
L’entrée /radius n’est pas retirée : la laisser ne coûte rien et permet de rebasculer sans
tout reconfigurer.
Ce que le retour ne peut pas rattraper — à afficher
La sauvegarde date d’avant la bascule. Les tickets vendus depuis vivent dans RADIUS et ne
sont pas dans ce fichier : ecart_avec_radius.avertissement les compte. Inversement, les
tickets déjà consommés depuis y figurent encore et redeviendront utilisables.
On ne corrige pas ça automatiquement — toute tentative ferait pire. On le dit, chiffre à l’appui, et l’administrateur tranche.
| Code d’erreur | Sens |
|---|---|
RESTORE_NO_BACKUP |
aucune sauvegarde des comptes pour ce routeur |
RESTORE_BACKUP_UNREADABLE |
sauvegarde illisible — refus, plutôt que restaurer zéro compte |
RESTORE_BACKUP_EMPTY |
la sauvegarde ne contient aucun compte restaurable |
RESTORE_USERS_FAILED |
restauration interrompue — RADIUS n’a pas été coupé |
RESTORE_INCOMPLETE |
moins de comptes que prévu — RADIUS reste actif |
5. Ce que le gérant reçoit, étape par étape
Notifications de type RADIUS_MIGRATION, une par étape :
| Étape | Message |
|---|---|
SAUVEGARDE |
« Sauvegarde de votre configuration (8 707 comptes) — faite. » |
IMPORT |
« Vos 8 707 comptes ont été copiés dans le serveur cloud. » |
RETRAIT_LOCAL |
« 8 706 comptes locaux retirés — ils vivent maintenant dans le cloud. » |
ARMEMENT |
« En attente de la première connexion d’un de vos clients. » |
ECHEC |
« La bascule s’est arrêtée : … Rien n’a été supprimé sur votre routeur. » |
Puis, type RADIUS_PREMIERE_AUTH, les cinq premières connexions réelles :
« Connexion 1/5 validée ✅ — TM-C4D835 vient de se connecter via le serveur cloud sur J+Pro Ax3. La bascule fonctionne. »
C’est la preuve que le gérant attend. Un « service opérationnel » sans nom d’usager ne prouve rien — c’est exactement ce qu’affichait l’ancien flux pendant que le hotspot continuait de répondre en local. Au-delà de cinq, le système se tait : il a compris qu’il a compris.
La confirmation est automatique depuis le 2026-08-29 (cron RADIUS_MIGRATION_WATCH, toutes
les 3 min). Auparavant elle exigeait un clic d’administrateur, et personne ne le faisait.
6. GET /affluence — ce que le gérant lit sur son réseau
Lecture ouverte au client : c’est son réseau.
{
"disponible": true,
"connexions": 128,
"clients": 34,
"refus": 3,
"pic": { "heure": 19, "connexions": 41 },
"connexions_par_client": 3.8,
"par_heure": [0, 0, "…"],
"boucles": []
}
connexions≠clients. Un même usager se réauthentifie plusieurs fois par session. Confondre les deux gonfle le chiffre d’un facteur 3 à 10 et ferait croire au gérant une affluence qu’il n’a pas. Affichez les deux.pic.heureest dans le fuseau du routeur. 23 h UTC, c’est minuit à Porto-Novo — un pic annoncé à la mauvaise heure ne sert à rien pour décider des horaires d’ouverture.bouclesliste les comptes qui se reconnectent plus de 200 fois par jour. Ce n’est pas de l’affluence, c’est une panne — signalez-le, ne le comptez pas comme fréquentation.disponible: falseavecmotif: "SOURCE_INDISPONIBLE": n’affichez pas un relevé à zéro. « 0 connexion » et « je ne sais pas » ne veulent pas dire la même chose.
Le relevé de fin de journée
Chaque soir à 20 h UTC (21 h au Bénin), type RADIUS_AFFLUENCE :
« 128 connexion(s) réussie(s) aujourd’hui sur J+Pro Ax3, pour 34 client(s). Pic de fréquentation vers 19 h. 3 connexion(s) refusée(s). »
Seuls les clients qui ont eu de l’activité sont notifiés. Envoyer « 0 connexion » chaque soir à un gérant dont le routeur dort est le meilleur moyen de lui faire couper les notifications — et il ne verrait plus les vraies.
7. Réglages
| Variable | Défaut | Effet |
|---|---|---|
RADIUS_MIGRATION_WATCH_ENABLED |
true |
confirmation auto + premières connexions, toutes les 3 min |
RADIUS_READY_AUTO_CONFIRM_ENABLED |
true |
(était false, et aucune cron ne l’appelait) |
RADIUS_AFFLUENCE_REPORT_ENABLED |
true |
relevé de fin de journée à 20 h UTC |