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
185
backend/internal/db/db.go
Normal file
185
backend/internal/db/db.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
// Package db gère la connexion SQLite et l'exécution des migrations.
|
||||
// Il expose une instance unique de base de données utilisée par tous les services.
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "modernc.org/sqlite" // Pilote SQLite pur Go (sans CGO)
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsFS embed.FS
|
||||
|
||||
// DB encapsule la connexion SQLite et expose les méthodes nécessaires.
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
// Open ouvre (ou crée) la base de données SQLite au chemin donné et exécute les migrations.
|
||||
func Open(dataDir string) (*DB, error) {
|
||||
if err := os.MkdirAll(dataDir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("création répertoire données : %w", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dataDir, "panel.db")
|
||||
|
||||
// Paramètres SQLite : WAL mode pour les lectures concurrentes, foreign keys activées
|
||||
dsn := fmt.Sprintf("file:%s?_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)&_pragma=busy_timeout(5000)", dbPath)
|
||||
sqlDB, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ouverture SQLite : %w", err)
|
||||
}
|
||||
|
||||
// Limiter les connexions simultanées (SQLite n'est pas conçu pour la concurrence élevée)
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
sqlDB.SetMaxIdleConns(1)
|
||||
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("connexion SQLite : %w", err)
|
||||
}
|
||||
|
||||
db := &DB{sqlDB}
|
||||
|
||||
// Exécuter les migrations manquantes
|
||||
if err := db.migrate(); err != nil {
|
||||
return nil, fmt.Errorf("migrations : %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// migrate applique les fichiers SQL de migration non encore exécutés.
|
||||
// Les fichiers sont numérotés (001_init.sql, 002_xxx.sql) et appliqués dans l'ordre.
|
||||
func (db *DB) migrate() error {
|
||||
// Créer la table schema_version si elle n'existe pas encore
|
||||
// (nécessaire avant de lire la version actuelle)
|
||||
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER NOT NULL,
|
||||
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("création schema_version : %w", err)
|
||||
}
|
||||
|
||||
// Lire la version actuelle
|
||||
var currentVersion int
|
||||
row := db.QueryRow(`SELECT COALESCE(MAX(version), 0) FROM schema_version`)
|
||||
if err := row.Scan(¤tVersion); err != nil {
|
||||
return fmt.Errorf("lecture version schéma : %w", err)
|
||||
}
|
||||
|
||||
// Lister et trier les fichiers de migration
|
||||
entries, err := migrationsFS.ReadDir("migrations")
|
||||
if err != nil {
|
||||
return fmt.Errorf("lecture dossier migrations : %w", err)
|
||||
}
|
||||
|
||||
type migration struct {
|
||||
version int
|
||||
name string
|
||||
}
|
||||
var migrations []migration
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") {
|
||||
continue
|
||||
}
|
||||
// Extraire le numéro de version depuis le nom du fichier (ex: "001_init.sql" → 1)
|
||||
parts := strings.SplitN(entry.Name(), "_", 2)
|
||||
if len(parts) < 1 {
|
||||
continue
|
||||
}
|
||||
v, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
migrations = append(migrations, migration{version: v, name: entry.Name()})
|
||||
}
|
||||
|
||||
// Trier par numéro de version croissant
|
||||
sort.Slice(migrations, func(i, j int) bool {
|
||||
return migrations[i].version < migrations[j].version
|
||||
})
|
||||
|
||||
// Appliquer les migrations manquantes
|
||||
for _, m := range migrations {
|
||||
if m.version <= currentVersion {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := migrationsFS.ReadFile("migrations/" + m.name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("lecture migration %s : %w", m.name, err)
|
||||
}
|
||||
|
||||
// Exécuter dans une transaction pour garantir l'atomicité
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("transaction migration %s : %w", m.name, err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(string(content)); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("exécution migration %s : %w", m.name, err)
|
||||
}
|
||||
|
||||
// Mettre à jour la version (la migration 001 l'insère elle-même, pas besoin de le refaire)
|
||||
if m.version > 1 {
|
||||
if _, err := tx.Exec(`INSERT INTO schema_version (version) VALUES (?)`, m.version); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("mise à jour version après migration %s : %w", m.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit migration %s : %w", m.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSetting lit un paramètre depuis la table settings.
|
||||
// Retourne "" et nil si la clé n'existe pas.
|
||||
func (db *DB) GetSetting(key string) (string, bool, error) {
|
||||
var value string
|
||||
var encrypted int
|
||||
err := db.QueryRow(`SELECT value, encrypted FROM settings WHERE key = ?`, key).Scan(&value, &encrypted)
|
||||
if err == sql.ErrNoRows {
|
||||
return "", false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return value, encrypted == 1, nil
|
||||
}
|
||||
|
||||
// SetSetting enregistre ou met à jour un paramètre.
|
||||
func (db *DB) SetSetting(key, value string, encrypted bool) error {
|
||||
enc := 0
|
||||
if encrypted {
|
||||
enc = 1
|
||||
}
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO settings (key, value, encrypted, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, encrypted=excluded.encrypted, updated_at=excluded.updated_at
|
||||
`, key, value, enc)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsInstalled vérifie si l'application a déjà été configurée.
|
||||
func (db *DB) IsInstalled() (bool, error) {
|
||||
v, _, err := db.GetSetting("installed")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return v == "true", nil
|
||||
}
|
||||
100
backend/internal/db/migrations/001_init.sql
Normal file
100
backend/internal/db/migrations/001_init.sql
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
-- Migration 001 : Schéma initial de ProxmoxPanel
|
||||
-- Crée toutes les tables de base nécessaires au CORE
|
||||
|
||||
-- Paramètres globaux de l'application (clé/valeur)
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
encrypted INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Utilisateurs (créés automatiquement au premier login)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
lang TEXT NOT NULL DEFAULT 'en',
|
||||
theme TEXT NOT NULL DEFAULT 'dark',
|
||||
sidebar_position TEXT NOT NULL DEFAULT 'left',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login_at DATETIME
|
||||
);
|
||||
|
||||
-- Sessions de refresh JWT (cookie httpOnly)
|
||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Modules disponibles et leur état (actif/inactif)
|
||||
CREATE TABLE IF NOT EXISTS modules (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
version TEXT NOT NULL DEFAULT '0.0.0',
|
||||
is_core INTEGER NOT NULL DEFAULT 0,
|
||||
is_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
installed_at DATETIME,
|
||||
config TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
-- Journal d'audit — toutes les actions sensibles
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
username TEXT,
|
||||
action TEXT NOT NULL,
|
||||
resource TEXT,
|
||||
details TEXT,
|
||||
ip TEXT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Widgets du dashboard par utilisateur
|
||||
CREATE TABLE IF NOT EXISTS user_widgets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
widget_type TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
config TEXT NOT NULL DEFAULT '{}',
|
||||
position_x INTEGER NOT NULL DEFAULT 0,
|
||||
position_y INTEGER NOT NULL DEFAULT 0,
|
||||
width INTEGER NOT NULL DEFAULT 2,
|
||||
height INTEGER NOT NULL DEFAULT 2,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Historique des mises à jour de paquets
|
||||
CREATE TABLE IF NOT EXISTS update_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_id TEXT NOT NULL UNIQUE,
|
||||
target TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
output TEXT NOT NULL DEFAULT '',
|
||||
started_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
finished_at DATETIME
|
||||
);
|
||||
|
||||
-- Version de schéma pour le système de migrations
|
||||
CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER NOT NULL,
|
||||
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO schema_version (version) VALUES (1);
|
||||
|
||||
-- Insertion des modules CORE par défaut (non désinstallables)
|
||||
INSERT OR IGNORE INTO modules (id, name, description, version, is_core, is_enabled) VALUES
|
||||
('dashboard', 'Dashboard', 'Tableau de bord avec widgets configurables', '1.0.0', 1, 1),
|
||||
('proxmox', 'Proxmox', 'Gestion des LXC et VM Proxmox', '1.0.0', 1, 1),
|
||||
('updates', 'Mises à jour', 'Mises à jour de paquets apt avec streaming', '1.0.0', 1, 1),
|
||||
('settings', 'Paramètres', 'Configuration de l''application', '1.0.0', 1, 1),
|
||||
('files', 'Fichiers', 'Navigateur de fichiers SFTP', '1.0.0', 0, 0),
|
||||
('terminal', 'Terminal', 'Terminal SSH interactif', '1.0.0', 0, 0),
|
||||
('logs', 'Logs', 'Streaming de logs en temps réel', '1.0.0', 0, 0),
|
||||
('services', 'Services', 'Gestion des services systemd', '1.0.0', 0, 0);
|
||||
Loading…
Add table
Add a link
Reference in a new issue