- install.go : detectPublicURL utilise https pour tout domaine public
même si Traefik envoie X-Forwarded-Proto: http en interne
- fr.json / en.json : échappe le @ dans proxmoxTokenHint avec {'@'}
(vue-i18n interprétait @realm comme un linked message → SyntaxError)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
265 lines
8.2 KiB
Go
265 lines
8.2 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.
|
|
// Traefik termine TLS en amont et communique en HTTP avec le backend.
|
|
// On fait confiance à X-Forwarded-Proto quand il vaut "https", et on suppose
|
|
// HTTPS pour tout domaine réel (avec un point) même si X-Forwarded-Proto est absent ou "http".
|
|
func detectPublicURL(r *http.Request) string {
|
|
host := r.Header.Get("X-Forwarded-Host")
|
|
if host == "" {
|
|
host = r.Host
|
|
}
|
|
|
|
fwdProto := r.Header.Get("X-Forwarded-Proto")
|
|
|
|
var proto string
|
|
switch {
|
|
case fwdProto == "https":
|
|
proto = "https"
|
|
case r.TLS != nil:
|
|
proto = "https"
|
|
case strings.Contains(host, ".") &&
|
|
!strings.HasPrefix(host, "localhost") &&
|
|
!strings.HasPrefix(host, "127.") &&
|
|
!strings.HasPrefix(host, "10.") &&
|
|
!strings.HasPrefix(host, "192.168."):
|
|
// Domaine public → toujours HTTPS
|
|
proto = "https"
|
|
default:
|
|
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
|
|
}
|