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>
206 lines
5.9 KiB
Go
206 lines
5.9 KiB
Go
// Handlers pour la page paramètres : lecture/écriture de la configuration globale.
|
|
package api
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"git.geronzi.fr/proxmoxPanel/core/backend/internal/audit"
|
|
"git.geronzi.fr/proxmoxPanel/core/backend/internal/db"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// SettingsHandler contient les handlers de configuration.
|
|
type SettingsHandler struct {
|
|
db *db.DB
|
|
auditLogger *audit.Logger
|
|
}
|
|
|
|
// NewSettingsHandler crée un SettingsHandler.
|
|
func NewSettingsHandler(database *db.DB, auditLog *audit.Logger) *SettingsHandler {
|
|
return &SettingsHandler{db: database, auditLogger: auditLog}
|
|
}
|
|
|
|
// paramètres publics (non-sensibles) accessibles par les admins.
|
|
var publicSettings = []string{
|
|
"instance_name",
|
|
"public_url",
|
|
"default_lang",
|
|
"proxmox_url",
|
|
"ssh_host",
|
|
"ssh_username",
|
|
}
|
|
|
|
// GetAll retourne tous les paramètres publics de l'application.
|
|
// GET /api/settings
|
|
func (h *SettingsHandler) GetAll(w http.ResponseWriter, r *http.Request) {
|
|
result := make(map[string]string)
|
|
for _, key := range publicSettings {
|
|
value, _, err := h.db.GetSetting(key)
|
|
if err == nil {
|
|
result[key] = value
|
|
}
|
|
}
|
|
JSONResponse(w, http.StatusOK, result)
|
|
}
|
|
|
|
// UpdateSetting met à jour un paramètre spécifique.
|
|
// PUT /api/settings/{key}
|
|
// Body: { "value": "..." }
|
|
func (h *SettingsHandler) UpdateSetting(w http.ResponseWriter, r *http.Request) {
|
|
claims := GetClaims(r)
|
|
|
|
key := chi.URLParam(r, "key")
|
|
if key == "" {
|
|
JSONError(w, "Clé de paramètre manquante", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Vérifier que la clé est modifiable
|
|
allowed := false
|
|
for _, k := range publicSettings {
|
|
if k == key {
|
|
allowed = true
|
|
break
|
|
}
|
|
}
|
|
if !allowed {
|
|
JSONError(w, "Paramètre non modifiable via l'API", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
Value string `json:"value"`
|
|
}
|
|
if err := decodeJSON(r, &body); err != nil {
|
|
JSONError(w, "Corps de requête invalide", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := h.db.SetSetting(key, body.Value, false); err != nil {
|
|
JSONError(w, "Erreur sauvegarde paramètre", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
h.auditLogger.Log(&claims.UserID, claims.Username, "setting_update", key,
|
|
map[string]string{"key": key}, clientIP(r))
|
|
|
|
JSONResponse(w, http.StatusOK, map[string]string{"message": "Paramètre mis à jour"})
|
|
}
|
|
|
|
// GetModules retourne la liste de tous les modules et leur état.
|
|
// GET /api/modules
|
|
func (h *SettingsHandler) GetModules(w http.ResponseWriter, r *http.Request) {
|
|
rows, err := h.db.Query(`
|
|
SELECT id, name, description, version, is_core, is_enabled, installed_at
|
|
FROM modules ORDER BY is_core DESC, name ASC
|
|
`)
|
|
if err != nil {
|
|
JSONError(w, "Erreur lecture modules", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
type module struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Version string `json:"version"`
|
|
IsCore bool `json:"is_core"`
|
|
IsEnabled bool `json:"is_enabled"`
|
|
InstalledAt *string `json:"installed_at,omitempty"`
|
|
}
|
|
|
|
var modules []module
|
|
for rows.Next() {
|
|
var m module
|
|
var isCore, isEnabled int
|
|
var installedAt *string
|
|
rows.Scan(&m.ID, &m.Name, &m.Description, &m.Version, &isCore, &isEnabled, &installedAt)
|
|
m.IsCore = isCore == 1
|
|
m.IsEnabled = isEnabled == 1
|
|
m.InstalledAt = installedAt
|
|
modules = append(modules, m)
|
|
}
|
|
|
|
JSONResponse(w, http.StatusOK, modules)
|
|
}
|
|
|
|
// EnableModule active un module.
|
|
// POST /api/modules/{id}/enable
|
|
func (h *SettingsHandler) EnableModule(w http.ResponseWriter, r *http.Request) {
|
|
claims := GetClaims(r)
|
|
id := chi.URLParam(r, "id")
|
|
|
|
result, err := h.db.Exec(`UPDATE modules SET is_enabled = 1 WHERE id = ?`, id)
|
|
if err != nil {
|
|
JSONError(w, "Erreur activation module", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
n, _ := result.RowsAffected()
|
|
if n == 0 {
|
|
JSONError(w, "Module introuvable", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
h.auditLogger.Log(&claims.UserID, claims.Username, "module_enable", id, nil, clientIP(r))
|
|
JSONResponse(w, http.StatusOK, map[string]string{"message": "Module activé (redémarrage requis pour prendre effet)"})
|
|
}
|
|
|
|
// DisableModule désactive un module (ne peut pas désactiver les modules CORE).
|
|
// POST /api/modules/{id}/disable
|
|
func (h *SettingsHandler) DisableModule(w http.ResponseWriter, r *http.Request) {
|
|
claims := GetClaims(r)
|
|
id := chi.URLParam(r, "id")
|
|
|
|
// Vérifier que ce n'est pas un module CORE
|
|
var isCore int
|
|
if err := h.db.QueryRow(`SELECT is_core FROM modules WHERE id = ?`, id).Scan(&isCore); err != nil {
|
|
JSONError(w, "Module introuvable", http.StatusNotFound)
|
|
return
|
|
}
|
|
if isCore == 1 {
|
|
JSONError(w, "Les modules CORE ne peuvent pas être désactivés", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
h.db.Exec(`UPDATE modules SET is_enabled = 0 WHERE id = ?`, id)
|
|
h.auditLogger.Log(&claims.UserID, claims.Username, "module_disable", id, nil, clientIP(r))
|
|
JSONResponse(w, http.StatusOK, map[string]string{"message": "Module désactivé"})
|
|
}
|
|
|
|
// GetAuditLog retourne le journal d'audit paginé.
|
|
// GET /api/settings/audit
|
|
func (h *SettingsHandler) GetAuditLog(w http.ResponseWriter, r *http.Request) {
|
|
rows, err := h.db.Query(`
|
|
SELECT id, username, action, resource, details, ip, created_at
|
|
FROM audit_log ORDER BY created_at DESC LIMIT 100
|
|
`)
|
|
if err != nil {
|
|
JSONError(w, "Erreur lecture audit", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
type entry struct {
|
|
ID int64 `json:"id"`
|
|
Username string `json:"username"`
|
|
Action string `json:"action"`
|
|
Resource *string `json:"resource,omitempty"`
|
|
Details *string `json:"details,omitempty"`
|
|
IP *string `json:"ip,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
var entries []entry
|
|
for rows.Next() {
|
|
var e entry
|
|
var resource, details, ip *string
|
|
rows.Scan(&e.ID, &e.Username, &e.Action, &resource, &details, &ip, &e.CreatedAt)
|
|
e.Resource = resource
|
|
e.Details = details
|
|
e.IP = ip
|
|
entries = append(entries, e)
|
|
}
|
|
|
|
JSONResponse(w, http.StatusOK, entries)
|
|
}
|