diff --git a/backend/internal/api/settings.go b/backend/internal/api/settings.go index 7dbbe5e..c78244d 100644 --- a/backend/internal/api/settings.go +++ b/backend/internal/api/settings.go @@ -6,6 +6,7 @@ import ( "strconv" "git.geronzi.fr/proxmoxPanel/core/backend/internal/audit" + "git.geronzi.fr/proxmoxPanel/core/backend/internal/crypto" "git.geronzi.fr/proxmoxPanel/core/backend/internal/db" "git.geronzi.fr/proxmoxPanel/core/backend/internal/logbuffer" "github.com/go-chi/chi/v5" @@ -15,11 +16,12 @@ import ( type SettingsHandler struct { db *db.DB auditLogger *audit.Logger + encryptor *crypto.Encryptor } // NewSettingsHandler crée un SettingsHandler. -func NewSettingsHandler(database *db.DB, auditLog *audit.Logger) *SettingsHandler { - return &SettingsHandler{db: database, auditLogger: auditLog} +func NewSettingsHandler(database *db.DB, auditLog *audit.Logger, enc *crypto.Encryptor) *SettingsHandler { + return &SettingsHandler{db: database, auditLogger: auditLog, encryptor: enc} } // paramètres publics (non-sensibles) accessibles par les admins. @@ -32,6 +34,9 @@ var publicSettings = []string{ "ssh_username", } +// paramètres sensibles : modifiables en écriture seule, stockés chiffrés. +var encryptedSettings = []string{"ssh_password", "proxmox_token"} + // GetAll retourne tous les paramètres publics de l'application. // GET /api/settings func (h *SettingsHandler) GetAll(w http.ResponseWriter, r *http.Request) { @@ -57,15 +62,22 @@ func (h *SettingsHandler) UpdateSetting(w http.ResponseWriter, r *http.Request) return } - // Vérifier que la clé est modifiable - allowed := false + // Vérifier que la clé est modifiable (publique ou chiffrée) + isPublic := false for _, k := range publicSettings { if k == key { - allowed = true + isPublic = true break } } - if !allowed { + isEncrypted := false + for _, k := range encryptedSettings { + if k == key { + isEncrypted = true + break + } + } + if !isPublic && !isEncrypted { JSONError(w, "Paramètre non modifiable via l'API", http.StatusForbidden) return } @@ -78,6 +90,27 @@ func (h *SettingsHandler) UpdateSetting(w http.ResponseWriter, r *http.Request) return } + // Paramètres chiffrés : valeur vide = ne pas modifier + if isEncrypted { + if body.Value == "" { + JSONResponse(w, http.StatusOK, map[string]string{"message": "Aucun changement"}) + return + } + encrypted, err := h.encryptor.Encrypt(body.Value) + if err != nil { + JSONError(w, "Erreur chiffrement", http.StatusInternalServerError) + return + } + if err := h.db.SetSetting(key, encrypted, true); 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"}) + return + } + if err := h.db.SetSetting(key, body.Value, false); err != nil { JSONError(w, "Erreur sauvegarde paramètre", http.StatusInternalServerError) return diff --git a/backend/main.go b/backend/main.go index 2060b11..f64b611 100644 --- a/backend/main.go +++ b/backend/main.go @@ -79,7 +79,7 @@ func main() { 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) + settingsHandler := api.NewSettingsHandler(database, auditLogger, encryptor) terminalHandler := api.NewTerminalHandler(database, auditLogger, encryptor) // Démarrer le polling Proxmox en arrière-plan diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index ec80fda..a664657 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -113,7 +113,10 @@ "defaultLang": "Default language", "sshHost": "SSH host", "sshUsername": "SSH username", + "sshPassword": "SSH password", "proxmoxUrl": "Proxmox URL", + "proxmoxToken": "Proxmox API token", + "secretPlaceholder": "Leave empty to keep current value", "darkMode": "Dark mode", "sidebarPosition": "Sidebar position", "left": "Left", diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index 4205b3c..13192ee 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -113,7 +113,10 @@ "defaultLang": "Langue par défaut", "sshHost": "Hôte SSH", "sshUsername": "Utilisateur SSH", + "sshPassword": "Mot de passe SSH", "proxmoxUrl": "URL Proxmox", + "proxmoxToken": "Token API Proxmox", + "secretPlaceholder": "Laisser vide pour ne pas modifier", "darkMode": "Mode sombre", "sidebarPosition": "Position de la sidebar", "left": "Gauche", diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index e1952ec..029db82 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -95,8 +95,7 @@ router.beforeEach(async (to) => { // Au premier chargement : vérifier l'installation ET restaurer la session if (!authStore.installChecked) { await authStore.checkInstallation() - // Restaurer la session depuis le cookie refresh si un token est en localStorage - await authStore.tryRefresh() + await authStore.restoreSession() } // Rediriger vers l'installation si pas encore configuré diff --git a/frontend/src/stores/auth.store.ts b/frontend/src/stores/auth.store.ts index b048287..8886723 100644 --- a/frontend/src/stores/auth.store.ts +++ b/frontend/src/stores/auth.store.ts @@ -70,9 +70,45 @@ export const useAuthStore = defineStore('auth', () => { scheduleRefresh(14 * 60 * 1000) } + /** + * Restaure la session au démarrage de l'application (après F5). + * 1. Essaie fetchMe() avec le token existant (marche si < 15 min) + * 2. Si le token est expiré, tente le refresh via le cookie httpOnly + */ + async function restoreSession(): Promise { + if (!accessToken.value) return + + // Le token est peut-être encore valide : évite d'avoir besoin du cookie + await fetchMe() + if (user.value) { + scheduleRefresh(14 * 60 * 1000) + return + } + + // Token expiré — tenter le refresh via le cookie httpOnly + try { + const res = await fetch('/api/auth/refresh', { + method: 'POST', + credentials: 'include', + }) + if (res.ok) { + const data = await res.json() + accessToken.value = data.access_token + localStorage.setItem('pxp_token', data.access_token) + await fetchMe() + if (user.value) scheduleRefresh(14 * 60 * 1000) + } else { + // Le refresh a explicitement échoué (cookie absent ou expiré) + clearSession() + } + } catch { + // Erreur réseau transitoire — ne pas effacer le token, laisser le guard rediriger + } + } + /** * Tente de renouveler le token via le cookie httpOnly (pxp_refresh). - * Appelé au démarrage de l'application. + * Utilisé par le timer automatique (14 min après login). */ async function tryRefresh(): Promise { const token = localStorage.getItem('pxp_token') @@ -81,19 +117,16 @@ export const useAuthStore = defineStore('auth', () => { try { const res = await fetch('/api/auth/refresh', { method: 'POST', - credentials: 'include', // Inclure le cookie httpOnly + credentials: 'include', }) if (res.ok) { const data = await res.json() accessToken.value = data.access_token localStorage.setItem('pxp_token', data.access_token) - - // Charger le profil utilisateur await fetchMe() scheduleRefresh(14 * 60 * 1000) } else { - // Refresh échoué — nettoyer la session clearSession() } } catch { @@ -177,6 +210,7 @@ export const useAuthStore = defineStore('auth', () => { checkInstallation, login, logout, + restoreSession, tryRefresh, fetchMe, updatePreferences, diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index 27c1165..ef5ae2c 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -55,10 +55,18 @@ +
+ + +
+
+ + +
@@ -173,6 +181,12 @@ const settings = ref({ proxmox_url: '', }) +// Champs sensibles — write-only, jamais retournés par l'API +const secrets = ref({ + ssh_password: '', + proxmox_token: '', +}) + onMounted(async () => { await loadSettings() await loadAuditLog() @@ -211,8 +225,13 @@ async function saveSettings() { saving.value = true saveSuccess.value = false - const keys = Object.entries(settings.value) - for (const [key, value] of keys) { + const allEntries: [string, string][] = [ + ...Object.entries(settings.value), + // Secrets : envoyés seulement si non-vides (le backend ignore les valeurs vides) + ...Object.entries(secrets.value).filter(([, v]) => v !== ''), + ] + + for (const [key, value] of allEntries) { await fetch(`/api/settings/${key}`, { method: 'PUT', headers: { @@ -223,6 +242,10 @@ async function saveSettings() { }) } + // Vider les champs secrets après sauvegarde + secrets.value.ssh_password = '' + secrets.value.proxmox_token = '' + saving.value = false saveSuccess.value = true setTimeout(() => (saveSuccess.value = false), 3000)