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 }