- Supprimer les modules services et logs du CORE (déplacés dans viewServices et viewLogs) - Enrichir modules/module.go : interface Registry avec NavItemDef, RunOnTarget, StreamOnTarget - Réécrire modules/loader.go : NewLoader accepte *db.DB, *sshpool.Pool, *crypto.Encryptor - Ajouter migration 005 : colonnes nav_* sur la table modules + suppression services/logs DB - Mettre à jour db.go (repairSchema) pour ajout idempotent des colonnes nav_* - Mettre à jour settings.go : GetModules retourne les champs nav, ajout GetRegistryModules et InstallRegistryModule - Mettre à jour main.go : NewLoader avec les bons arguments, ajout routes /api/registry/modules - Mettre à jour modules.html : section Store avec liste des modules Forgejo - Mettre à jour app.js : sidebar dynamique (nav_href depuis DB), modulesPage avec store - Mettre à jour pages.css : styles pour store de modules Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
227 lines
6.5 KiB
Go
227 lines
6.5 KiB
Go
package modules
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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"
|
|
)
|
|
|
|
// Loader charge et gère les modules actifs.
|
|
type Loader struct {
|
|
db *db.DB
|
|
pool *sshpool.Pool
|
|
enc *crypto.Encryptor
|
|
registry *coreRegistry
|
|
modules []Module
|
|
}
|
|
|
|
// NewLoader crée un Loader avec les services du CORE nécessaires aux modules.
|
|
func NewLoader(database *db.DB, pool *sshpool.Pool, enc *crypto.Encryptor) *Loader {
|
|
return &Loader{
|
|
db: database,
|
|
pool: pool,
|
|
enc: enc,
|
|
registry: newCoreRegistry(database.DB, pool, enc),
|
|
}
|
|
}
|
|
|
|
// RegisterModule enregistre un module disponible (appelé à l'init, depuis main.go).
|
|
func (l *Loader) RegisterModule(m Module) {
|
|
l.modules = append(l.modules, m)
|
|
}
|
|
|
|
// LoadActive charge et initialise tous les modules activés en base de données.
|
|
func (l *Loader) LoadActive() error {
|
|
for _, m := range l.modules {
|
|
enabled, err := l.isEnabled(m.ID())
|
|
if err != nil {
|
|
return fmt.Errorf("vérification module %s : %w", m.ID(), err)
|
|
}
|
|
if !enabled {
|
|
log.Printf("Module %s : désactivé, ignoré", m.ID())
|
|
continue
|
|
}
|
|
|
|
log.Printf("Module %s : chargement...", m.ID())
|
|
if err := m.Register(l.registry); err != nil {
|
|
return fmt.Errorf("initialisation module %s : %w", m.ID(), err)
|
|
}
|
|
log.Printf("Module %s : chargé avec succès", m.ID())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// isEnabled vérifie en base de données si un module est activé.
|
|
func (l *Loader) isEnabled(id string) (bool, error) {
|
|
var enabled int
|
|
err := l.db.QueryRow(`SELECT is_enabled FROM modules WHERE id = ?`, id).Scan(&enabled)
|
|
if err == sql.ErrNoRows {
|
|
return false, nil
|
|
}
|
|
return enabled == 1, err
|
|
}
|
|
|
|
// Registry retourne le registry partagé.
|
|
func (l *Loader) Registry() *coreRegistry {
|
|
return l.registry
|
|
}
|
|
|
|
// ---- Implémentation interne du Registry ----
|
|
|
|
type RouteEntry struct {
|
|
Method string
|
|
Path string
|
|
Handler http.HandlerFunc
|
|
RequireAdmin bool
|
|
}
|
|
|
|
type migrationEntry struct {
|
|
version int
|
|
sql string
|
|
fn MigrationFn
|
|
}
|
|
|
|
type translationEntry struct {
|
|
lang string
|
|
keys map[string]string
|
|
}
|
|
|
|
// coreRegistry implémente l'interface Registry.
|
|
type coreRegistry struct {
|
|
sqlDB *sql.DB
|
|
pool *sshpool.Pool
|
|
enc *crypto.Encryptor
|
|
routes []RouteEntry
|
|
wsChannels map[string]WSHandler
|
|
widgets []WidgetDef
|
|
settingsTabs []SettingsTabDef
|
|
migrations []migrationEntry
|
|
translations []translationEntry
|
|
navItems map[string]NavItemDef // nav items en mémoire (clé = module ID)
|
|
}
|
|
|
|
func newCoreRegistry(sqlDB *sql.DB, pool *sshpool.Pool, enc *crypto.Encryptor) *coreRegistry {
|
|
return &coreRegistry{
|
|
sqlDB: sqlDB,
|
|
pool: pool,
|
|
enc: enc,
|
|
wsChannels: make(map[string]WSHandler),
|
|
navItems: make(map[string]NavItemDef),
|
|
}
|
|
}
|
|
|
|
func (r *coreRegistry) RegisterRoute(method, path string, handler http.HandlerFunc, requireAdmin bool) {
|
|
r.routes = append(r.routes, RouteEntry{method, path, handler, requireAdmin})
|
|
}
|
|
|
|
func (r *coreRegistry) RegisterWSChannel(channel string, handler WSHandler) {
|
|
r.wsChannels[channel] = handler
|
|
}
|
|
|
|
func (r *coreRegistry) RegisterWidget(widget WidgetDef) {
|
|
r.widgets = append(r.widgets, widget)
|
|
}
|
|
|
|
func (r *coreRegistry) RegisterSettingsTab(tab SettingsTabDef) {
|
|
r.settingsTabs = append(r.settingsTabs, tab)
|
|
}
|
|
|
|
func (r *coreRegistry) RegisterTranslations(lang string, keys map[string]string) {
|
|
r.translations = append(r.translations, translationEntry{lang, keys})
|
|
}
|
|
|
|
func (r *coreRegistry) RegisterMigration(version int, sqlStr string, fn MigrationFn) {
|
|
r.migrations = append(r.migrations, migrationEntry{version, sqlStr, fn})
|
|
}
|
|
|
|
// RegisterNavItem enregistre l'entrée de navigation d'un module en mémoire et en DB.
|
|
func (r *coreRegistry) RegisterNavItem(item NavItemDef) {
|
|
r.navItems[item.ID] = item
|
|
// Persister en DB pour que le frontend puisse le récupérer via /api/modules
|
|
r.sqlDB.Exec(
|
|
`UPDATE modules SET nav_href=?, nav_icon=?, nav_color=?, nav_label_key=? WHERE id=?`,
|
|
item.Href, item.Icon, item.Color, item.LabelKey, item.ID,
|
|
)
|
|
}
|
|
|
|
// DB retourne la connexion SQLite brute.
|
|
func (r *coreRegistry) DB() *sql.DB {
|
|
return r.sqlDB
|
|
}
|
|
|
|
// RunOnTarget exécute une commande SSH sur la cible (host ou lxc:VMID).
|
|
// La commande est wrappée via pct exec pour les cibles LXC.
|
|
func (r *coreRegistry) RunOnTarget(target, command string) (string, error) {
|
|
host, user, pass, err := r.sshCreds()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
cmd := buildTargetCmd(target, command)
|
|
return r.pool.RunCommand(host, user, pass, cmd)
|
|
}
|
|
|
|
// StreamOnTarget exécute une commande SSH en streaming sur la cible.
|
|
func (r *coreRegistry) StreamOnTarget(target, command string, output chan<- string) error {
|
|
host, user, pass, err := r.sshCreds()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd := buildTargetCmd(target, command)
|
|
return r.pool.StreamCommand(host, user, pass, cmd, output)
|
|
}
|
|
|
|
// sshCreds récupère et déchiffre les credentials SSH depuis la configuration.
|
|
func (r *coreRegistry) sshCreds() (host, user, pass string, err error) {
|
|
var h, u, ep string
|
|
r.sqlDB.QueryRow(`SELECT value FROM settings WHERE key='ssh_host'`).Scan(&h)
|
|
r.sqlDB.QueryRow(`SELECT value FROM settings WHERE key='ssh_username'`).Scan(&u)
|
|
r.sqlDB.QueryRow(`SELECT value FROM settings WHERE key='ssh_password'`).Scan(&ep)
|
|
if ep != "" {
|
|
pass, err = r.enc.Decrypt(ep)
|
|
if err != nil {
|
|
return "", "", "", fmt.Errorf("impossible de déchiffrer le mot de passe SSH")
|
|
}
|
|
}
|
|
if h == "" || u == "" || pass == "" {
|
|
return "", "", "", fmt.Errorf("SSH non configuré")
|
|
}
|
|
return h, u, pass, nil
|
|
}
|
|
|
|
// buildTargetCmd construit la commande pour la cible (host ou lxc:VMID).
|
|
func buildTargetCmd(target, command string) string {
|
|
if strings.HasPrefix(target, "lxc:") {
|
|
vmid := strings.TrimPrefix(target, "lxc:")
|
|
if _, err := strconv.Atoi(vmid); err == nil {
|
|
return fmt.Sprintf("pct exec %s -- sh -c %q", vmid, command)
|
|
}
|
|
}
|
|
return command
|
|
}
|
|
|
|
// GetRoutes retourne les routes enregistrées par les modules.
|
|
func (r *coreRegistry) GetRoutes() []RouteEntry {
|
|
return r.routes
|
|
}
|
|
|
|
// GetNavItems retourne les nav items enregistrés en mémoire.
|
|
func (r *coreRegistry) GetNavItems() map[string]NavItemDef {
|
|
return r.navItems
|
|
}
|
|
|
|
// GetWidgets retourne les types de widgets disponibles.
|
|
func (r *coreRegistry) GetWidgets() []WidgetDef {
|
|
return r.widgets
|
|
}
|
|
|
|
// GetSettingsTabs retourne les onglets de paramètres des modules.
|
|
func (r *coreRegistry) GetSettingsTabs() []SettingsTabDef {
|
|
return r.settingsTabs
|
|
}
|