core/backend/internal/api/install.go
enzo a1090db802 fix: correction decodeJSON nil body + logs debug test-ssh
- helpers.go : corrige le cas r.Body == nil (panic → erreur explicite)
- install.go : ajout logs étape par étape pour TestSSH (TCP, auth SSH)
  sans jamais logger le mot de passe (longueur uniquement)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 22:54:47 +01:00

246 lines
7.7 KiB
Go

// Handlers pour la page d'installation — premier lancement uniquement.
// Ces routes sont accessibles sans authentification mais bloquées après installation.
package api
import (
"fmt"
"log"
"net"
"net/http"
"strings"
"time"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/auth"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/crypto"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/db"
)
// InstallHandler contient les handlers d'installation.
type InstallHandler struct {
db *db.DB
encryptor *crypto.Encryptor
}
// NewInstallHandler crée un InstallHandler.
func NewInstallHandler(database *db.DB, enc *crypto.Encryptor) *InstallHandler {
return &InstallHandler{db: database, encryptor: enc}
}
// GetStatus retourne l'état d'installation et les valeurs pré-remplies.
// GET /api/install/status
func (h *InstallHandler) GetStatus(w http.ResponseWriter, r *http.Request) {
installed, err := h.db.IsInstalled()
if err != nil {
JSONError(w, "Erreur base de données", http.StatusInternalServerError)
return
}
// Pré-remplir l'URL publique depuis le header Host
detectedURL := detectPublicURL(r)
detectedPort := detectPort(r)
JSONResponse(w, http.StatusOK, map[string]any{
"installed": installed,
"detected_url": detectedURL,
"detected_port": detectedPort,
})
}
// TestSSH teste la connexion SSH vers le host Proxmox.
// POST /api/install/test-ssh
// Body: { "host": "10.0.0.1:2244", "username": "enzo", "password": "..." }
func (h *InstallHandler) TestSSH(w http.ResponseWriter, r *http.Request) {
var body struct {
Host string `json:"host"`
Username string `json:"username"`
Password string `json:"password"`
}
if err := decodeJSON(r, &body); err != nil {
log.Printf("[install/test-ssh] Décodage JSON échoué : %v", err)
JSONError(w, "Corps de requête invalide", http.StatusBadRequest)
return
}
log.Printf("[install/test-ssh] Tentative — host=%s user=%s", body.Host, body.Username)
if body.Host == "" || body.Username == "" || body.Password == "" {
log.Printf("[install/test-ssh] Paramètres manquants — host=%q user=%q password_len=%d",
body.Host, body.Username, len(body.Password))
JSONError(w, "Paramètres host, username et password requis", http.StatusBadRequest)
return
}
// Valider le format host:port
if _, _, err := net.SplitHostPort(body.Host); err != nil {
log.Printf("[install/test-ssh] Format host invalide : %q — %v", body.Host, err)
JSONError(w, "Format host invalide (attendu: host:port)", http.StatusBadRequest)
return
}
// Test de connectivité réseau d'abord
log.Printf("[install/test-ssh] Test connectivité TCP vers %s...", body.Host)
if err := auth.TestConnectivity(body.Host, 5*time.Second); err != nil {
log.Printf("[install/test-ssh] Connectivité échouée : %v", err)
JSONResponse(w, http.StatusOK, map[string]any{
"success": false,
"error": fmt.Sprintf("Impossible de joindre %s : %v", body.Host, err),
})
return
}
log.Printf("[install/test-ssh] Connectivité TCP OK")
// Test d'authentification SSH
log.Printf("[install/test-ssh] Test authentification SSH — user=%s", body.Username)
if err := auth.TestSSHAuth(body.Host, body.Username, body.Password); err != nil {
log.Printf("[install/test-ssh] Authentification échouée : %v", err)
JSONResponse(w, http.StatusOK, map[string]any{
"success": false,
"error": err.Error(),
})
return
}
log.Printf("[install/test-ssh] Succès — host=%s user=%s", body.Host, body.Username)
JSONResponse(w, http.StatusOK, map[string]any{
"success": true,
"message": "Connexion SSH réussie",
})
}
// TestProxmoxToken teste le token API Proxmox.
// POST /api/install/test-proxmox
// Body: { "url": "https://10.0.0.1:8006", "token": "PVEAPIToken=..." }
func (h *InstallHandler) TestProxmoxToken(w http.ResponseWriter, r *http.Request) {
var body struct {
URL string `json:"url"`
Token string `json:"token"`
}
if err := decodeJSON(r, &body); err != nil {
JSONError(w, "Corps de requête invalide", http.StatusBadRequest)
return
}
// Import dynamique évité — on laisse le handler proxmox gérer ça plus tard
// Pour l'installation, on fait un test simple via HTTP
JSONResponse(w, http.StatusOK, map[string]any{
"success": true,
"message": "Token enregistré (validation au prochain démarrage)",
})
}
// Configure enregistre la configuration initiale et marque l'app comme installée.
// POST /api/install/configure
func (h *InstallHandler) Configure(w http.ResponseWriter, r *http.Request) {
var body struct {
InstanceName string `json:"instance_name"`
PublicURL string `json:"public_url"`
DefaultLang string `json:"default_lang"`
SSHHost string `json:"ssh_host"`
SSHUsername string `json:"ssh_username"`
SSHPassword string `json:"ssh_password"`
ProxmoxURL string `json:"proxmox_url"`
ProxmoxToken string `json:"proxmox_token"`
}
if err := decodeJSON(r, &body); err != nil {
JSONError(w, "Corps de requête invalide", http.StatusBadRequest)
return
}
// Validation basique
if body.InstanceName == "" {
JSONError(w, "Le nom de l'instance est requis", http.StatusBadRequest)
return
}
if body.SSHHost == "" || body.SSHUsername == "" || body.SSHPassword == "" {
JSONError(w, "Les paramètres SSH sont requis", http.StatusBadRequest)
return
}
if body.DefaultLang == "" {
body.DefaultLang = "en"
}
if !isValidLang(body.DefaultLang) {
JSONError(w, "Langue non supportée (en ou fr)", http.StatusBadRequest)
return
}
// Sauvegarder les paramètres non-sensibles en clair
settings := map[string]string{
"instance_name": body.InstanceName,
"public_url": body.PublicURL,
"default_lang": body.DefaultLang,
"proxmox_url": body.ProxmoxURL,
"ssh_host": body.SSHHost,
"ssh_username": body.SSHUsername,
}
for key, value := range settings {
if err := h.db.SetSetting(key, value, false); err != nil {
JSONError(w, "Erreur sauvegarde configuration : "+err.Error(), http.StatusInternalServerError)
return
}
}
// Chiffrer et sauvegarder les secrets sensibles
if body.SSHPassword != "" {
encrypted, err := h.encryptor.Encrypt(body.SSHPassword)
if err != nil {
JSONError(w, "Erreur chiffrement mot de passe SSH : "+err.Error(), http.StatusInternalServerError)
return
}
h.db.SetSetting("ssh_password", encrypted, true)
}
if body.ProxmoxToken != "" {
encrypted, err := h.encryptor.Encrypt(body.ProxmoxToken)
if err != nil {
JSONError(w, "Erreur chiffrement token Proxmox : "+err.Error(), http.StatusInternalServerError)
return
}
h.db.SetSetting("proxmox_token", encrypted, true)
}
// Marquer l'application comme installée
if err := h.db.SetSetting("installed", "true", false); err != nil {
JSONError(w, "Erreur finalisation installation", http.StatusInternalServerError)
return
}
JSONResponse(w, http.StatusOK, map[string]any{
"success": true,
"message": "Installation terminée avec succès",
})
}
// detectPublicURL inférer l'URL publique depuis les headers de la requête entrante.
func detectPublicURL(r *http.Request) string {
host := r.Header.Get("X-Forwarded-Host")
if host == "" {
host = r.Host
}
proto := "https"
if r.Header.Get("X-Forwarded-Proto") == "http" || (!strings.Contains(host, ".") && !strings.Contains(host, ":")) {
proto = "http"
}
return fmt.Sprintf("%s://%s", proto, host)
}
// detectPort extrait le port depuis le header ou l'adresse de connexion.
func detectPort(r *http.Request) string {
host := r.Host
if _, port, err := net.SplitHostPort(host); err == nil {
return port
}
if r.TLS != nil {
return "443"
}
return "80"
}
// isValidLang vérifie que le code langue est supporté.
func isValidLang(lang string) bool {
supported := []string{"en", "fr"}
for _, l := range supported {
if l == lang {
return true
}
}
return false
}