core/backend/main.go
enzo 5dbcb1df07 feat: initialisation complète du CORE ProxmoxPanel
Backend Go 1.23+ :
- API REST + WebSocket (chi, gorilla/websocket)
- Authentification PAM via SSH + JWT RS256
- Chiffrement AES-256-GCM pour secrets SQLite
- Pool SSH, client Proxmox REST, hub WebSocket pub/sub
- Système de modules compilés à initialisation conditionnelle
- Audit log, migrations SQLite versionnées

Frontend Vue 3 + Vite + TypeScript :
- Thème Neumorphism sombre/clair (CSS custom properties)
- Wizard d'installation, Dashboard drag-drop, Terminal xterm.js
- Toutes les vues CORE + stubs modules optionnels
- i18n EN/FR (vue-i18n v11)

Infrastructure :
- Docker multi-stage (Go → alpine, Node → nginx)
- docker-compose.yml, .gitattributes, LICENSE MIT, README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 21:08:53 +01:00

214 lines
8.4 KiB
Go

// ProxmoxPanel — CORE Backend
// Point d'entrée du serveur Go. Initialise la base de données, les services,
// enregistre les modules actifs et démarre le serveur HTTP sur :3001.
package main
import (
"log"
"net/http"
"os"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/api"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/audit"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/auth"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/crypto"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/db"
sshpool "git.geronzi.fr/proxmoxPanel/core/backend/internal/ssh"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/websocket"
"git.geronzi.fr/proxmoxPanel/core/backend/modules"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
// Répertoire de données persistantes (volume Docker)
dataDir := getEnv("DATA_DIR", "/app/data")
log.Printf("ProxmoxPanel CORE — démarrage (data: %s)", dataDir)
// ── Initialisation de la base de données ───────────────────────────────
database, err := db.Open(dataDir)
if err != nil {
log.Fatalf("Impossible d'ouvrir la base de données : %v", err)
}
log.Println("Base de données SQLite initialisée")
// ── Services de base ───────────────────────────────────────────────────
encryptor, err := crypto.NewEncryptor(dataDir)
if err != nil {
log.Fatalf("Impossible d'initialiser le chiffrement : %v", err)
}
log.Println("Chiffrement AES-256-GCM initialisé")
jwtManager, err := auth.NewJWTManager(dataDir)
if err != nil {
log.Fatalf("Impossible d'initialiser JWT : %v", err)
}
log.Println("Clés JWT RS256 prêtes")
// SSH host depuis la configuration (peut être vide si pas encore installé)
sshHost, _, _ := database.GetSetting("ssh_host")
var sshAuthenticator *auth.SSHAuthenticator
if sshHost != "" {
sshAuthenticator = auth.NewSSHAuthenticator(sshHost)
} else {
sshAuthenticator = auth.NewSSHAuthenticator("") // Sera mis à jour après installation
}
sshPool := sshpool.NewPool()
defer sshPool.Close()
hub := websocket.NewHub()
auditLogger := audit.New(database.DB)
// ── Chargement des modules actifs ──────────────────────────────────────
loader := modules.NewLoader(database.DB)
// Les modules sont enregistrés ici (compilés dans le binaire)
// loader.RegisterModule(dashboard.New(...)) ← à décommenter quand implémentés
if err := loader.LoadActive(); err != nil {
log.Fatalf("Erreur chargement modules : %v", err)
}
// ── Handlers HTTP ──────────────────────────────────────────────────────
installHandler := api.NewInstallHandler(database, encryptor)
authHandler := api.NewAuthHandler(database, jwtManager, sshAuthenticator, auditLogger)
proxmoxHandler := api.NewProxmoxHandler(database, hub, auditLogger, encryptor)
updatesHandler := api.NewUpdatesHandler(database, sshPool, hub, auditLogger, encryptor)
settingsHandler := api.NewSettingsHandler(database, auditLogger)
terminalHandler := api.NewTerminalHandler(database, auditLogger, encryptor)
// Démarrer le polling Proxmox en arrière-plan
proxmoxHandler.StartPolling()
// ── Router Chi ─────────────────────────────────────────────────────────
r := chi.NewRouter()
// Middlewares globaux
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.RequestID)
r.Use(api.SecurityHeaders)
r.Use(middleware.Compress(5)) // Compression gzip
// Limiter global (100 req/min par IP)
globalLimiter := api.NewRateLimiter(100, 60*1000000000) // 60 secondes
r.Use(api.RateLimit(globalLimiter))
// ── Routes publiques (sans authentification) ───────────────────────────
r.Get("/api/health", func(w http.ResponseWriter, r *http.Request) {
api.JSONResponse(w, http.StatusOK, map[string]string{"status": "ok"})
})
// Routes d'installation (accessibles seulement si non-installé)
r.Group(func(r chi.Router) {
r.Use(requireNotInstalled(database))
r.Get("/api/install/status", installHandler.GetStatus)
r.Post("/api/install/test-ssh", installHandler.TestSSH)
r.Post("/api/install/test-proxmox", installHandler.TestProxmoxToken)
r.Post("/api/install/configure", installHandler.Configure)
})
// Status d'installation accessible toujours (pour la redirection frontend)
r.Get("/api/install/check", func(w http.ResponseWriter, r *http.Request) {
installed, _ := database.IsInstalled()
api.JSONResponse(w, http.StatusOK, map[string]bool{"installed": installed})
})
// Routes d'authentification
r.Post("/api/auth/login", authHandler.Login)
r.Post("/api/auth/refresh", authHandler.Refresh)
// ── Routes protégées (JWT requis) ──────────────────────────────────────
r.Group(func(r chi.Router) {
r.Use(api.RequireAuth(jwtManager))
r.Post("/api/auth/logout", authHandler.Logout)
r.Get("/api/auth/me", authHandler.Me)
r.Patch("/api/auth/preferences", authHandler.UpdatePreferences)
// Proxmox
r.Get("/api/proxmox/resources", proxmoxHandler.GetResources)
r.Get("/api/proxmox/lxc", proxmoxHandler.GetLXC)
// Actions Proxmox — admin uniquement
r.Group(func(r chi.Router) {
r.Use(api.RequireAdmin)
r.Post("/api/proxmox/lxc/{vmid}/start", proxmoxHandler.StartLXC)
r.Post("/api/proxmox/lxc/{vmid}/stop", proxmoxHandler.StopLXC)
})
// Mises à jour — admin uniquement
r.Group(func(r chi.Router) {
r.Use(api.RequireAdmin)
r.Post("/api/updates/run", updatesHandler.RunUpdate)
})
r.Get("/api/updates/history", updatesHandler.GetHistory)
// Paramètres — admin uniquement
r.Group(func(r chi.Router) {
r.Use(api.RequireAdmin)
r.Get("/api/settings", settingsHandler.GetAll)
r.Put("/api/settings/{key}", settingsHandler.UpdateSetting)
r.Get("/api/settings/audit", settingsHandler.GetAuditLog)
})
// Modules
r.Get("/api/modules", settingsHandler.GetModules)
r.Group(func(r chi.Router) {
r.Use(api.RequireAdmin)
r.Post("/api/modules/{id}/enable", settingsHandler.EnableModule)
r.Post("/api/modules/{id}/disable", settingsHandler.DisableModule)
})
// WebSocket — les routes WS extraient le token via query param
r.Get("/ws/proxmox", proxmoxHandler.WebSocket)
r.Get("/ws/updates/{jobId}", updatesHandler.WebSocketUpdate)
r.Get("/ws/terminal", terminalHandler.WebSocket)
})
// Routes enregistrées par les modules actifs
for _, route := range loader.Registry().GetRoutes() {
routeCopy := route // Capturer la variable pour la closure
if routeCopy.RequireAdmin {
r.With(api.RequireAuth(jwtManager), api.RequireAdmin).MethodFunc(routeCopy.Method, routeCopy.Path, routeCopy.Handler)
} else {
r.With(api.RequireAuth(jwtManager)).MethodFunc(routeCopy.Method, routeCopy.Path, routeCopy.Handler)
}
}
// Servir les assets frontend (en production, c'est Nginx qui s'en charge)
if _, err := os.Stat("./static"); err == nil {
fs := http.FileServer(http.Dir("./static"))
r.Handle("/*", fs)
}
// ── Démarrage du serveur ───────────────────────────────────────────────
addr := getEnv("LISTEN_ADDR", ":3001")
log.Printf("Serveur démarré sur %s", addr)
if err := http.ListenAndServe(addr, r); err != nil {
log.Fatalf("Serveur arrêté : %v", err)
}
}
// requireNotInstalled est un middleware qui bloque les routes d'installation si déjà installé.
func requireNotInstalled(database *db.DB) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// La route /api/install/status reste accessible pour le check
installed, _ := database.IsInstalled()
if installed {
api.JSONError(w, "Application déjà installée", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// getEnv lit une variable d'environnement avec une valeur par défaut.
func getEnv(key, defaultValue string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultValue
}