Files
Loki/internal/ajean/web_link_api.go
T
nathaninline 110458c133 Renommage AJEAN (1/3) : module, chemins de donnees et auto-MAJ
Socle du renommage jean -> AJEAN, sans aucun impact visible pour le parc
installe. Trois briques :

- module github.com/nathaninline/ajean, cmd/ajean, internal/ajean,
  package ajean. Purement interne.

- AjeanHome() remplace JeanHome(). Les defauts deviennent
  %ProgramData%\ajean et /etc/ajean, avec migration du dossier existant
  au premier lancement. La migration est un os.Rename dans le dossier
  parent de l'ancien chemin : intra-volume, donc atomique et instantane
  meme avec des .gguf de plusieurs dizaines de Go. Si le rename echoue
  (handle ouvert sous Windows, droits, volume en lecture seule) on
  CONTINUE sur l'ancien chemin et on retentera : jamais de copie, jamais
  de suppression, le pire cas est "rien n'a bouge".
  $AJEAN_HOME et $JEAN_HOME restent tous deux honores, ainsi que
  /etc/default/ajean et /etc/default/jean : un chemin impose a la main
  est un ordre, on ne le migre pas.

- l'auto-MAJ accepte desormais ajean-<os>-<arch> ET jean-<os>-<arch>,
  dans cet ordre. Indispensable dans les deux sens : les binaires deja
  installes ne connaissent que jean-*, et ce binaire-ci doit pouvoir
  s'installer depuis une release anterieure au renommage.

Couvert par des tests, dont le repli quand un handle ouvert bloque le
rename sous Windows.
2026-08-04 13:59:25 +02:00

101 lines
4.0 KiB
Go

package ajean
// web_link_api.go — API locale pilotant l'accès distant (ajean.link) depuis l'UI
// web de Jean, pour éviter le terminal. Le panneau « Accès distant » ouvre une
// popup app.ajean.link/connect.html qui gère compte + paiement puis renvoie une
// clé de liaison (jl_…) ; l'UI la POSTe ici, et Jean fait le `jean link` tout
// seul (écrit le token + (re)démarre le service).
//
// Ces routes ne sont accessibles qu'en LOCAL : à travers le tunnel du relais,
// newLinkHandler refuse tout /api/* non chiffré (boîte noire). La configuration
// de l'accès distant se fait donc depuis la machine elle-même, ce qui est correct.
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
// remoteServerURL est l'URL du portail distant pointant droit sur cette machine.
func remoteServerURL() string {
return "https://app.ajean.link/server.html?m=" + machineID()
}
// handleLinkStatus (GET /api/link/status) : état de l'accès distant pour l'UI.
func handleLinkStatus(w http.ResponseWriter, r *http.Request) {
tok := readLinkToken()
sendJSON(w, http.StatusOK, map[string]any{
"linked": tok != "",
"active": linkServiceActive(),
"machineURL": remoteServerURL(),
"fingerprint": e2eFingerprint(),
})
}
// handleLinkConnect (POST /api/link/connect {token}) : enregistre la clé de
// liaison remise par la popup connect.html et (re)démarre le service de lien.
func handleLinkConnect(w http.ResponseWriter, r *http.Request) {
var req struct{ Token string }
_ = json.NewDecoder(r.Body).Decode(&req)
tok := strings.TrimSpace(req.Token)
if !strings.HasPrefix(tok, "jl_") {
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "clé de liaison invalide"})
return
}
if err := saveLinkToken(tok); err != nil {
sendJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error()})
return
}
// (re)démarre le worker pour qu'il prenne la nouvelle clé. Sur une machine sans
// systemd (dev/Windows), le token est quand même enregistré : on renvoie l'info
// mais on signale que le service n'a pas pu démarrer ici.
svcErr := ""
if err := startLink(true); err != nil {
svcErr = err.Error()
}
sendJSON(w, http.StatusOK, map[string]any{
"ok": true,
"linked": true,
"active": linkServiceActive(),
"machineURL": remoteServerURL(),
"serviceErr": svcErr,
})
}
// handleLinkStart (POST /api/link/start) : (re)démarre le worker de lien sans
// repasser par la popup de connexion — le token est déjà enregistré. Utile quand
// le tunnel est tombé, ou sur un poste sans systemd où personne ne le relance.
func handleLinkStart(w http.ResponseWriter, r *http.Request) {
if readLinkToken() == "" {
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "aucune clé de liaison enregistrée"})
return
}
if err := linkServiceCtl("restart"); err != nil {
sendJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error(), "active": linkServiceActive()})
return
}
sendJSON(w, http.StatusOK, map[string]any{"ok": true, "active": linkServiceActive()})
}
// handleLinkDisconnect (POST /api/link/disconnect) : arrête le service et oublie
// la clé (équivalent `jean link stop` + `jean link logout`).
func handleLinkDisconnect(w http.ResponseWriter, r *http.Request) {
_ = linkServiceCtl("stop")
_ = removeLinkToken()
sendJSON(w, http.StatusOK, map[string]any{"ok": true, "linked": false, "active": false})
}
// handleLinkPairCode (POST /api/link/paircode) : génère un code d'appairage frais
// (usage unique, TTL 10 min) + l'empreinte E2E, à saisir UNE fois dans le portail
// distant lors de la première connexion (confirmation de la boîte noire). Évite le
// détour par `jean link code` en terminal.
func handleLinkPairCode(w http.ResponseWriter, r *http.Request) {
code, err := newPairCode()
if err != nil {
sendJSON(w, http.StatusInternalServerError, map[string]any{"error": fmt.Sprintf("génération du code (droits ?): %v", err)})
return
}
sendJSON(w, http.StatusOK, map[string]any{"code": code, "fingerprint": e2eFingerprint()})
}