From 9a93702313cd5386d9af30d9e88f582a71a884f1 Mon Sep 17 00:00:00 2001 From: nathaninline Date: Mon, 13 Jul 2026 12:39:37 +0200 Subject: [PATCH] web: panneau Acces OpenAI (endpoint /v1 + cle API masquee, gestion depuis le web) Nouvel endpoint /api/apikey (GET etat + POST generate/set/clear -> restart). Section sidebar affichant l'URL compatible OpenAI et la cle API masquee par defaut (reveler/copier/generer/definir/retirer). Refactor config.go: helpers genAPIKey/writeAPIKey/maskAPIKey partages CLI + web. --- config.go | 47 ++++++++++++++++++++++++++++++++----------- ui/index.html | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++- web.go | 45 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 12 deletions(-) diff --git a/config.go b/config.go index fe6ec9e..43516ea 100644 --- a/config.go +++ b/config.go @@ -31,6 +31,40 @@ func authHeader(req *http.Request) { } } +// genAPIKey returns a fresh random OpenAI-style completion key. +func genAPIKey() string { + buf := make([]byte, 24) + _, _ = rand.Read(buf) + return "sk-jean-" + hex.EncodeToString(buf) +} + +// writeAPIKey persists (key != "") or clears (key == "") the completion API key +// in $JEAN_HOME/.api_key. It also wipes any residual API_KEY in config.env to +// avoid ambiguity. It does NOT restart the service — callers decide when to +// apply (llama-server only reads --api-key at launch). +func writeAPIKey(key string) error { + _ = SetConfigKey("API_KEY", "") + if key == "" { + if err := os.Remove(apiKeyPath()); err != nil && !os.IsNotExist(err) { + return err + } + return nil + } + return os.WriteFile(apiKeyPath(), []byte(key+"\n"), 0o600) +} + +// maskAPIKey renders a key for display: keep the "sk-jean-" prefix and last 4 +// chars, elide the middle. Empty in → empty out. +func maskAPIKey(k string) string { + if k == "" { + return "" + } + if len(k) <= 12 { + return "…" + k[len(k)-2:] + } + return k[:8] + "…" + k[len(k)-4:] +} + // cmdSetAPIKey sets (or clears) the API key stored in $JEAN_HOME/.api_key. When // exposed on the internet, llama-server requires "Authorization: Bearer " // for every call. @@ -42,11 +76,7 @@ func cmdSetAPIKey(args []string) error { var key string switch { case len(args) == 0: - buf := make([]byte, 24) - if _, err := rand.Read(buf); err != nil { - return err - } - key = "sk-jean-" + hex.EncodeToString(buf) + key = genAPIKey() fmt.Printf("%s clé générée : %s\n", green("[ok]"), bold(key)) case args[0] == "" || args[0] == "off" || args[0] == "none": key = "" @@ -55,12 +85,7 @@ func cmdSetAPIKey(args []string) error { } // Stocke dans le fichier dédié (survit aux switches de preset). On nettoie // aussi un éventuel API_KEY résiduel dans config.env pour éviter la confusion. - _ = SetConfigKey("API_KEY", "") - if key == "" { - if err := os.Remove(apiKeyPath()); err != nil && !os.IsNotExist(err) { - return err - } - } else if err := os.WriteFile(apiKeyPath(), []byte(key+"\n"), 0o600); err != nil { + if err := writeAPIKey(key); err != nil { return err } if key == "" { diff --git a/ui/index.html b/ui/index.html index 0105abf..0b65f84 100644 --- a/ui/index.html +++ b/ui/index.html @@ -329,6 +329,25 @@ button:disabled{opacity:.5;cursor:not-allowed}
System prompt
+
Accès OpenAI +
Endpoint compatible OpenAI?Branche n'importe quel client compatible OpenAI (OpenCode, Continue, Cursor…) sur cette URL. Modèle : jean. Si une clé est définie, le client doit envoyer Authorization: Bearer <clé>.
+
+ + +
+
modèle : jean
+
Clé API?Protège l'endpoint des complétions (llama-server). Indispensable avant d'exposer le serveur sur internet. Modifier la clé redémarre le service.
+
+ + + +
+
+ + + +
+
Service
@@ -726,6 +745,40 @@ function renderInternet(s){ else { st.innerHTML='⚠️ serveur injoignable — les outils web ne seront pas proposés'; } } async function loadInternet(){ renderInternet(await jget('/api/internet')); } +// --- Accès OpenAI (endpoint /v1 + clé API des complétions) ----------------- +let OAI_KEY='', OAI_REVEAL=false; +async function copyText(txt, msg){ + if(!txt){ toast('rien à copier'); return; } + try{ await navigator.clipboard.writeText(txt); } + catch(_){ const ta=document.createElement('textarea'); ta.value=txt; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); } + toast(msg||'copié'); +} +function copyApiKey(){ copyText(OAI_KEY, OAI_KEY?'clé copiée':'aucune clé'); } +function renderApiKey(d){ + OAI_KEY = d.key || ''; + // URL de l'endpoint : llama-server tourne sur d.port (≠ port de l'UI web). + const proto = location.protocol==='https:' ? 'https:' : 'http:'; + document.getElementById('oai-url').value = proto+'//'+location.hostname+':'+d.port+'/v1'; + const inp=document.getElementById('oai-key'); + if(!d.set){ inp.value='(aucune clé — serveur ouvert)'; inp.style.opacity=.6; } + else { inp.style.opacity=1; inp.value = OAI_REVEAL ? OAI_KEY : d.masked; } + document.getElementById('oai-key-eye').style.display = d.set ? '' : 'none'; +} +async function loadApiKey(){ renderApiKey(await jget('/api/apikey')); } +function toggleKeyReveal(){ OAI_REVEAL=!OAI_REVEAL; const inp=document.getElementById('oai-key'); if(OAI_KEY) inp.value = OAI_REVEAL ? OAI_KEY : (OAI_KEY.slice(0,8)+'…'+OAI_KEY.slice(-4)); } +async function apiKeyAction(action){ + if(action==='clear' && !await askConfirm('Retirer la clé rend l\'endpoint OpenAI accessible SANS authentification. Le service va redémarrer.', {title:'Retirer la clé API ?', okText:'Retirer'})) return; + if(action==='generate' && OAI_KEY && !await askConfirm('Générer une nouvelle clé invalide l\'ancienne (les clients devront être mis à jour). Le service va redémarrer.', {title:'Régénérer la clé ?', okText:'Générer'})) return; + OAI_REVEAL=(action==='generate'); // révèle la clé fraîche pour qu'on puisse la copier + toast('application…'); + renderApiKey(await jpost('/api/apikey', {action})); +} +async function apiKeySet(){ + const k = await askPrompt('Colle ta clé API (ou laisse vide pour annuler) :', {title:'Définir la clé API', placeholder:'sk-…'}); + if(!k || !k.trim()) return; + OAI_REVEAL=true; toast('application…'); + renderApiKey(await jpost('/api/apikey', {action:'set', key:k.trim()})); +} async function toggleInternet(){ const on=document.getElementById('internet-toggle').checked; const url=document.getElementById('crawl-url').value.trim(); @@ -736,7 +789,7 @@ async function saveCrawlUrl(){ const url=document.getElementById('crawl-url').value.trim(); renderInternet(await jpost('/api/internet',{url})); } -async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadAgent(),loadInternet()]); } +async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadAgent(),loadInternet(),loadApiKey()]); } async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); } function openBenchModal(){ document.getElementById('bench-modal').style.display = 'flex'; } function closeBenchModal(){ document.getElementById('bench-modal').style.display = 'none'; } diff --git a/web.go b/web.go index fabb17b..4874c95 100644 --- a/web.go +++ b/web.go @@ -86,6 +86,7 @@ func newWebMux() *http.ServeMux { api("/api/agent", handleAgent) api("/api/agent/toggle", handleAgentToggle) api("/api/agent/tool-limit", handleToolLimitToggle) + api("/api/apikey", handleAPIKey) api("/api/internet", handleInternet) api("/api/memory", handleMemoryMode) // Alias rétro-compat : l'ancien portail ajean.link (dépôt jean-relay) pilote @@ -505,6 +506,50 @@ func handleAgentToggle(w http.ResponseWriter, r *http.Request) { // // GET → {enabled, url, reachable} // POST {enabled, url} → enregistre CRAWL4AI_URL + le drapeau .internet_enabled +// handleAPIKey expose et pilote la clé d'accès à l'endpoint compatible OpenAI +// (llama-server /v1). GET renvoie l'état ; POST {action:"generate"|"set"|"clear", +// key?} l'écrit puis redémarre le service (llama-server lit --api-key au lancement). +func handleAPIKey(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + var req struct { + Action string `json:"action"` + Key string `json:"key"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()}) + return + } + var key string + switch req.Action { + case "generate": + key = genAPIKey() + case "set": + key = strings.TrimSpace(req.Key) + case "clear": + key = "" + default: + sendJSON(w, 400, map[string]any{"ok": false, "error": "action inconnue"}) + return + } + if err := writeAPIKey(key); err != nil { + sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()}) + return + } + // La clé n'est appliquée qu'au (re)démarrage de llama-server. + if serviceIsActive() { + _ = serviceAction("restart") + } + } + k := readAPIKey() + sendJSON(w, 200, map[string]any{ + "ok": true, + "set": k != "", + "key": k, + "masked": maskAPIKey(k), + "port": LLMPort(), + }) +} + func handleInternet(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { var req struct {