SOGOMS v1.0.7 - 2FA obligatoire et Infrastructure Management

Phase 17g - Double Authentification:
- TOTP avec Google Authenticator/Authy
- QR code pour enrôlement
- Codes de backup (10 codes usage unique)
- Page /admin/security pour gestion 2FA
- Page /admin/users avec Reset 2FA (super_admin)
- 2FA obligatoire pour rôles configurés

Phase 21 - Infrastructure Management:
- SQLite pour données infra (/data/infra.db)
- SSH Pool avec reconnexion auto
- Gestion Incus (list, start, stop, restart, sync)
- Gestion Nginx (test, reload, deploy, sync, certbot)
- Interface admin /admin/infra
- Formulaire ajout serveur
- Page détail serveur avec containers et sites

Fichiers créés:
- internal/infra/ (db, models, migrations, repository, ssh, incus, nginx)
- cmd/sogoms/admin/totp.go
- cmd/sogoms/admin/handlers_2fa.go
- cmd/sogoms/admin/handlers_infra.go
- Templates: 2fa_*, security, users, infra, server_*

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-26 21:21:11 +01:00
parent 1274400b08
commit 0b1977e0c4
37 changed files with 4976 additions and 148 deletions

View File

@@ -19,10 +19,12 @@ type Schema struct {
// Table représente une table de la base de données.
type Table struct {
Columns map[string]*Column `yaml:"columns"`
Primary []string `yaml:"primary,omitempty"` // Clé primaire composite
CRUD []string `yaml:"crud"`
Order string `yaml:"order,omitempty"`
Columns map[string]*Column `yaml:"columns"`
Primary []string `yaml:"primary,omitempty"` // Clé primaire composite
CRUD []string `yaml:"crud"`
Order string `yaml:"order,omitempty"`
SoftDelete bool `yaml:"soft_delete,omitempty"` // Si true, DELETE → UPDATE deleted_at
Cascade bool `yaml:"cascade,omitempty"` // Si true, soft delete en cascade sur enfants
}
// Column représente une colonne d'une table.
@@ -78,6 +80,50 @@ func (t *Table) IsCompositePK() bool {
return len(t.Primary) > 1
}
// IsSoftDelete vérifie si la table utilise le soft delete.
func (t *Table) IsSoftDelete() bool {
return t.SoftDelete
}
// IsCascade vérifie si le cascade delete est activé.
func (t *Table) IsCascade() bool {
return t.Cascade
}
// ChildRelation représente une relation enfant (FK vers table parent).
type ChildRelation struct {
ChildTable string // Nom de la table enfant
ChildColumn string // Colonne FK dans la table enfant
ParentTable string // Nom de la table parent
}
// GetChildRelations retourne les tables enfants qui ont une FK vers parentTable.
func (s *Schema) GetChildRelations(parentTable string) []ChildRelation {
var children []ChildRelation
for tableName, table := range s.Tables {
if tableName == parentTable {
continue // Skip la table elle-même
}
for colName, col := range table.Columns {
if col.Foreign != "" {
// col.Foreign = "table.column"
parts := strings.Split(col.Foreign, ".")
if len(parts) >= 1 && parts[0] == parentTable {
children = append(children, ChildRelation{
ChildTable: tableName,
ChildColumn: colName,
ParentTable: parentTable,
})
}
}
}
}
return children
}
// GetSelectColumns retourne la liste des colonnes pour un SELECT.
func (t *Table) GetSelectColumns() []string {
cols := make([]string, 0, len(t.Columns))
@@ -111,13 +157,26 @@ func (t *Table) GetUpdateColumns() []string {
}
// BuildListQuery génère la requête SELECT pour list.
// Filtre automatiquement les enregistrements supprimés si soft_delete est activé.
func (t *Table) BuildListQuery(tableName string) string {
cols := t.GetSelectColumns()
query := fmt.Sprintf("SELECT %s FROM %s", strings.Join(cols, ", "), tableName)
// Ajouter filtre owner si présent
// Construire les conditions WHERE
var conditions []string
// Filtre owner si présent
if ownerCol := t.GetOwnerColumn(); ownerCol != "" {
query += fmt.Sprintf(" WHERE %s = ?", ownerCol)
conditions = append(conditions, fmt.Sprintf("%s = ?", ownerCol))
}
// Filtre soft delete
if t.SoftDelete {
conditions = append(conditions, "deleted_at IS NULL")
}
if len(conditions) > 0 {
query += " WHERE " + strings.Join(conditions, " AND ")
}
// Ajouter ORDER BY si défini
@@ -129,6 +188,7 @@ func (t *Table) BuildListQuery(tableName string) string {
}
// BuildShowQuery génère la requête SELECT pour show (un seul enregistrement).
// Filtre automatiquement les enregistrements supprimés si soft_delete est activé.
func (t *Table) BuildShowQuery(tableName string) string {
cols := t.GetSelectColumns()
pk := t.GetPrimaryKey()
@@ -139,6 +199,11 @@ func (t *Table) BuildShowQuery(tableName string) string {
query += fmt.Sprintf(" AND %s = ?", ownerCol)
}
// Filtre soft delete
if t.SoftDelete {
query += " AND deleted_at IS NULL"
}
return query
}