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>
470 lines
15 KiB
Go
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
|
|
}
|