// 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" "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 { JSONError(w, "Corps de requête invalide", http.StatusBadRequest) return } if body.Host == "" || body.Username == "" || 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 { JSONError(w, "Format host invalide (attendu: host:port)", http.StatusBadRequest) return } // Test de connectivité réseau d'abord if err := auth.TestConnectivity(body.Host, 5*time.Second); err != nil { JSONResponse(w, http.StatusOK, map[string]any{ "success": false, "error": fmt.Sprintf("Impossible de joindre %s : %v", body.Host, err), }) return } // Test d'authentification SSH if err := auth.TestSSHAuth(body.Host, body.Username, body.Password); err != nil { JSONResponse(w, http.StatusOK, map[string]any{ "success": false, "error": err.Error(), }) return } 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 }