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>
This commit is contained in:
commit
5dbcb1df07
66 changed files with 10370 additions and 0 deletions
206
backend/internal/api/settings.go
Normal file
206
backend/internal/api/settings.go
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
// 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue