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>
214 lines
8.4 KiB
Go
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
|
|
}
|