Files
sogoms/internal/infra/repository.go
Pierre 0b1977e0c4 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>
2025-12-26 21:21:11 +01:00

470 lines
15 KiB
Go

// Package infra gère l'infrastructure (serveurs, containers, nginx).
package infra
import (
"database/sql"
"fmt"
"time"
)
// ============================================================================
// Servers
// ============================================================================
// CreateServer crée un nouveau serveur.
func (db *DB) CreateServer(s *Server) error {
now := time.Now()
result, err := db.Exec(`
INSERT INTO servers (name, host, vpn_ip, ssh_port, ssh_user, ssh_key_file, has_incus, has_nginx, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
s.Name, s.Host, nullString(s.VpnIP), s.SSHPort, s.SSHUser, s.SSHKeyFile,
boolToInt(s.HasIncus), boolToInt(s.HasNginx), s.Status, now, now)
if err != nil {
return fmt.Errorf("insert server: %w", err)
}
id, _ := result.LastInsertId()
s.ID = id
s.CreatedAt = now
s.UpdatedAt = now
return nil
}
// GetServer récupère un serveur par ID.
func (db *DB) GetServer(id int64) (*Server, error) {
s := &Server{}
err := db.QueryRow(`
SELECT id, name, host, vpn_ip, ssh_port, ssh_user, ssh_key_file, has_incus, has_nginx, status, created_at, updated_at
FROM servers WHERE id = ?`, id).Scan(
&s.ID, &s.Name, &s.Host, &s.VpnIP, &s.SSHPort, &s.SSHUser, &s.SSHKeyFile,
&s.HasIncus, &s.HasNginx, &s.Status, &s.CreatedAt, &s.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("get server: %w", err)
}
return s, nil
}
// GetServerByName récupère un serveur par nom.
func (db *DB) GetServerByName(name string) (*Server, error) {
s := &Server{}
err := db.QueryRow(`
SELECT id, name, host, vpn_ip, ssh_port, ssh_user, ssh_key_file, has_incus, has_nginx, status, created_at, updated_at
FROM servers WHERE name = ?`, name).Scan(
&s.ID, &s.Name, &s.Host, &s.VpnIP, &s.SSHPort, &s.SSHUser, &s.SSHKeyFile,
&s.HasIncus, &s.HasNginx, &s.Status, &s.CreatedAt, &s.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("get server by name: %w", err)
}
return s, nil
}
// ListServers retourne tous les serveurs.
func (db *DB) ListServers() ([]Server, error) {
rows, err := db.Query(`
SELECT id, name, host, vpn_ip, ssh_port, ssh_user, ssh_key_file, has_incus, has_nginx, status, created_at, updated_at
FROM servers ORDER BY name`)
if err != nil {
return nil, fmt.Errorf("list servers: %w", err)
}
defer rows.Close()
var servers []Server
for rows.Next() {
var s Server
if err := rows.Scan(&s.ID, &s.Name, &s.Host, &s.VpnIP, &s.SSHPort, &s.SSHUser, &s.SSHKeyFile,
&s.HasIncus, &s.HasNginx, &s.Status, &s.CreatedAt, &s.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan server: %w", err)
}
servers = append(servers, s)
}
return servers, nil
}
// UpdateServer met à jour un serveur.
func (db *DB) UpdateServer(s *Server) error {
s.UpdatedAt = time.Now()
_, err := db.Exec(`
UPDATE servers SET name=?, host=?, vpn_ip=?, ssh_port=?, ssh_user=?, ssh_key_file=?,
has_incus=?, has_nginx=?, status=?, updated_at=? WHERE id=?`,
s.Name, s.Host, nullString(s.VpnIP), s.SSHPort, s.SSHUser, s.SSHKeyFile,
boolToInt(s.HasIncus), boolToInt(s.HasNginx), s.Status, s.UpdatedAt, s.ID)
if err != nil {
return fmt.Errorf("update server: %w", err)
}
return nil
}
// UpdateServerStatus met à jour le statut d'un serveur.
func (db *DB) UpdateServerStatus(id int64, status ServerStatus) error {
_, err := db.Exec(`UPDATE servers SET status=?, updated_at=? WHERE id=?`,
status, time.Now(), id)
if err != nil {
return fmt.Errorf("update server status: %w", err)
}
return nil
}
// DeleteServer supprime un serveur.
func (db *DB) DeleteServer(id int64) error {
_, err := db.Exec(`DELETE FROM servers WHERE id=?`, id)
if err != nil {
return fmt.Errorf("delete server: %w", err)
}
return nil
}
// ============================================================================
// Containers
// ============================================================================
// CreateContainer crée un nouveau container.
func (db *DB) CreateContainer(c *Container) error {
now := time.Now()
result, err := db.Exec(`
INSERT INTO containers (server_id, name, incus_name, ip, vpn_ip, image, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
c.ServerID, c.Name, c.IncusName, nullString(c.IP), nullString(c.VpnIP), c.Image, c.Status, now, now)
if err != nil {
return fmt.Errorf("insert container: %w", err)
}
id, _ := result.LastInsertId()
c.ID = id
c.CreatedAt = now
c.UpdatedAt = now
return nil
}
// GetContainer récupère un container par ID.
func (db *DB) GetContainer(id int64) (*Container, error) {
c := &Container{}
err := db.QueryRow(`
SELECT id, server_id, name, incus_name, ip, vpn_ip, image, status, created_at, updated_at
FROM containers WHERE id = ?`, id).Scan(
&c.ID, &c.ServerID, &c.Name, &c.IncusName, &c.IP, &c.VpnIP, &c.Image, &c.Status, &c.CreatedAt, &c.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("get container: %w", err)
}
return c, nil
}
// ListContainersByServer retourne les containers d'un serveur.
func (db *DB) ListContainersByServer(serverID int64) ([]Container, error) {
rows, err := db.Query(`
SELECT id, server_id, name, incus_name, ip, vpn_ip, image, status, created_at, updated_at
FROM containers WHERE server_id = ? ORDER BY name`, serverID)
if err != nil {
return nil, fmt.Errorf("list containers: %w", err)
}
defer rows.Close()
var containers []Container
for rows.Next() {
var c Container
if err := rows.Scan(&c.ID, &c.ServerID, &c.Name, &c.IncusName, &c.IP, &c.VpnIP, &c.Image, &c.Status, &c.CreatedAt, &c.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan container: %w", err)
}
containers = append(containers, c)
}
return containers, nil
}
// ListAllContainers retourne tous les containers.
func (db *DB) ListAllContainers() ([]Container, error) {
rows, err := db.Query(`
SELECT id, server_id, name, incus_name, ip, vpn_ip, image, status, created_at, updated_at
FROM containers ORDER BY server_id, name`)
if err != nil {
return nil, fmt.Errorf("list all containers: %w", err)
}
defer rows.Close()
var containers []Container
for rows.Next() {
var c Container
if err := rows.Scan(&c.ID, &c.ServerID, &c.Name, &c.IncusName, &c.IP, &c.VpnIP, &c.Image, &c.Status, &c.CreatedAt, &c.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan container: %w", err)
}
containers = append(containers, c)
}
return containers, nil
}
// UpdateContainer met à jour un container.
func (db *DB) UpdateContainer(c *Container) error {
c.UpdatedAt = time.Now()
_, err := db.Exec(`
UPDATE containers SET server_id=?, name=?, incus_name=?, ip=?, vpn_ip=?, image=?, status=?, updated_at=?
WHERE id=?`,
c.ServerID, c.Name, c.IncusName, nullString(c.IP), nullString(c.VpnIP), c.Image, c.Status, c.UpdatedAt, c.ID)
if err != nil {
return fmt.Errorf("update container: %w", err)
}
return nil
}
// UpdateContainerStatus met à jour le statut d'un container.
func (db *DB) UpdateContainerStatus(id int64, status ContainerStatus) error {
_, err := db.Exec(`UPDATE containers SET status=?, updated_at=? WHERE id=?`,
status, time.Now(), id)
if err != nil {
return fmt.Errorf("update container status: %w", err)
}
return nil
}
// DeleteContainer supprime un container.
func (db *DB) DeleteContainer(id int64) error {
_, err := db.Exec(`DELETE FROM containers WHERE id=?`, id)
if err != nil {
return fmt.Errorf("delete container: %w", err)
}
return nil
}
// ============================================================================
// NginxConfigs
// ============================================================================
// CreateNginxConfig crée une nouvelle config Nginx.
func (db *DB) CreateNginxConfig(n *NginxConfig) error {
now := time.Now()
result, err := db.Exec(`
INSERT INTO nginx_configs (server_id, domain, type, template, upstream, ssl_enabled, config_content, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
n.ServerID, n.Domain, n.Type, nullString(n.Template), nullString(n.Upstream),
boolToInt(n.SSLEnabled), nullString(n.ConfigContent), n.Status, now, now)
if err != nil {
return fmt.Errorf("insert nginx config: %w", err)
}
id, _ := result.LastInsertId()
n.ID = id
n.CreatedAt = now
n.UpdatedAt = now
return nil
}
// GetNginxConfig récupère une config Nginx par ID.
func (db *DB) GetNginxConfig(id int64) (*NginxConfig, error) {
n := &NginxConfig{}
err := db.QueryRow(`
SELECT id, server_id, domain, type, template, upstream, ssl_enabled, config_content, status, created_at, updated_at
FROM nginx_configs WHERE id = ?`, id).Scan(
&n.ID, &n.ServerID, &n.Domain, &n.Type, &n.Template, &n.Upstream,
&n.SSLEnabled, &n.ConfigContent, &n.Status, &n.CreatedAt, &n.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("get nginx config: %w", err)
}
return n, nil
}
// GetNginxConfigByDomain récupère une config par domaine.
func (db *DB) GetNginxConfigByDomain(serverID int64, domain string) (*NginxConfig, error) {
n := &NginxConfig{}
err := db.QueryRow(`
SELECT id, server_id, domain, type, template, upstream, ssl_enabled, config_content, status, created_at, updated_at
FROM nginx_configs WHERE server_id = ? AND domain = ?`, serverID, domain).Scan(
&n.ID, &n.ServerID, &n.Domain, &n.Type, &n.Template, &n.Upstream,
&n.SSLEnabled, &n.ConfigContent, &n.Status, &n.CreatedAt, &n.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("get nginx config by domain: %w", err)
}
return n, nil
}
// ListNginxConfigsByServer retourne les configs d'un serveur.
func (db *DB) ListNginxConfigsByServer(serverID int64) ([]NginxConfig, error) {
rows, err := db.Query(`
SELECT id, server_id, domain, type, template, upstream, ssl_enabled, config_content, status, created_at, updated_at
FROM nginx_configs WHERE server_id = ? ORDER BY domain`, serverID)
if err != nil {
return nil, fmt.Errorf("list nginx configs: %w", err)
}
defer rows.Close()
var configs []NginxConfig
for rows.Next() {
var n NginxConfig
if err := rows.Scan(&n.ID, &n.ServerID, &n.Domain, &n.Type, &n.Template, &n.Upstream,
&n.SSLEnabled, &n.ConfigContent, &n.Status, &n.CreatedAt, &n.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan nginx config: %w", err)
}
configs = append(configs, n)
}
return configs, nil
}
// ListAllNginxConfigs retourne toutes les configs.
func (db *DB) ListAllNginxConfigs() ([]NginxConfig, error) {
rows, err := db.Query(`
SELECT id, server_id, domain, type, template, upstream, ssl_enabled, config_content, status, created_at, updated_at
FROM nginx_configs ORDER BY server_id, domain`)
if err != nil {
return nil, fmt.Errorf("list all nginx configs: %w", err)
}
defer rows.Close()
var configs []NginxConfig
for rows.Next() {
var n NginxConfig
if err := rows.Scan(&n.ID, &n.ServerID, &n.Domain, &n.Type, &n.Template, &n.Upstream,
&n.SSLEnabled, &n.ConfigContent, &n.Status, &n.CreatedAt, &n.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan nginx config: %w", err)
}
configs = append(configs, n)
}
return configs, nil
}
// UpdateNginxConfig met à jour une config.
func (db *DB) UpdateNginxConfig(n *NginxConfig) error {
n.UpdatedAt = time.Now()
_, err := db.Exec(`
UPDATE nginx_configs SET server_id=?, domain=?, type=?, template=?, upstream=?,
ssl_enabled=?, config_content=?, status=?, updated_at=? WHERE id=?`,
n.ServerID, n.Domain, n.Type, nullString(n.Template), nullString(n.Upstream),
boolToInt(n.SSLEnabled), nullString(n.ConfigContent), n.Status, n.UpdatedAt, n.ID)
if err != nil {
return fmt.Errorf("update nginx config: %w", err)
}
return nil
}
// DeleteNginxConfig supprime une config.
func (db *DB) DeleteNginxConfig(id int64) error {
_, err := db.Exec(`DELETE FROM nginx_configs WHERE id=?`, id)
if err != nil {
return fmt.Errorf("delete nginx config: %w", err)
}
return nil
}
// ============================================================================
// AppBindings
// ============================================================================
// CreateAppBinding crée un nouveau binding.
func (db *DB) CreateAppBinding(b *AppBinding) error {
now := time.Now()
result, err := db.Exec(`
INSERT INTO app_bindings (app_id, container_id, nginx_config_id, server_id, type, created_at)
VALUES (?, ?, ?, ?, ?, ?)`,
b.AppID, nullInt64(b.ContainerID), nullInt64(b.NginxConfigID), nullInt64(b.ServerID), b.Type, now)
if err != nil {
return fmt.Errorf("insert app binding: %w", err)
}
id, _ := result.LastInsertId()
b.ID = id
b.CreatedAt = now
return nil
}
// ListAppBindings retourne les bindings d'une app.
func (db *DB) ListAppBindings(appID string) ([]AppBinding, error) {
rows, err := db.Query(`
SELECT id, app_id, container_id, nginx_config_id, server_id, type, created_at
FROM app_bindings WHERE app_id = ?`, appID)
if err != nil {
return nil, fmt.Errorf("list app bindings: %w", err)
}
defer rows.Close()
var bindings []AppBinding
for rows.Next() {
var b AppBinding
if err := rows.Scan(&b.ID, &b.AppID, &b.ContainerID, &b.NginxConfigID, &b.ServerID, &b.Type, &b.CreatedAt); err != nil {
return nil, fmt.Errorf("scan app binding: %w", err)
}
bindings = append(bindings, b)
}
return bindings, nil
}
// DeleteAppBinding supprime un binding.
func (db *DB) DeleteAppBinding(id int64) error {
_, err := db.Exec(`DELETE FROM app_bindings WHERE id=?`, id)
if err != nil {
return fmt.Errorf("delete app binding: %w", err)
}
return nil
}
// DeleteAppBindingsByApp supprime tous les bindings d'une app.
func (db *DB) DeleteAppBindingsByApp(appID string) error {
_, err := db.Exec(`DELETE FROM app_bindings WHERE app_id=?`, appID)
if err != nil {
return fmt.Errorf("delete app bindings: %w", err)
}
return nil
}
// ============================================================================
// Helpers
// ============================================================================
func nullString(s string) interface{} {
if s == "" {
return nil
}
return s
}
func nullInt64(i *int64) interface{} {
if i == nil {
return nil
}
return *i
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
// ============================================================================
// Aggregate queries
// ============================================================================
// GetInfraOverview retourne une vue complète de l'infrastructure.
func (db *DB) GetInfraOverview() (*InfraOverview, error) {
servers, err := db.ListServers()
if err != nil {
return nil, err
}
overview := &InfraOverview{
Servers: make([]ServerWithContainers, len(servers)),
}
for i, s := range servers {
overview.Servers[i].Server = s
containers, err := db.ListContainersByServer(s.ID)
if err != nil {
return nil, err
}
overview.Servers[i].Containers = containers
}
overview.NginxConfigs, err = db.ListAllNginxConfigs()
if err != nil {
return nil, err
}
return overview, nil
}