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:
306
internal/infra/nginx.go
Normal file
306
internal/infra/nginx.go
Normal file
@@ -0,0 +1,306 @@
|
||||
// Package infra gère l'infrastructure (serveurs, containers, nginx).
|
||||
package infra
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// NginxSitesAvailable est le répertoire des sites disponibles.
|
||||
NginxSitesAvailable = "/etc/nginx/sites-available"
|
||||
// NginxSitesEnabled est le répertoire des sites activés.
|
||||
NginxSitesEnabled = "/etc/nginx/sites-enabled"
|
||||
)
|
||||
|
||||
// NginxSiteInfo représente les infos d'un site Nginx.
|
||||
type NginxSiteInfo struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
Config string
|
||||
HasSSL bool
|
||||
Domains []string
|
||||
Upstream string
|
||||
}
|
||||
|
||||
// TestNginxConfig teste la configuration Nginx.
|
||||
func (c *SSHClient) TestNginxConfig(ctx context.Context) error {
|
||||
result, err := c.Exec(ctx, "nginx -t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("nginx test: %w", err)
|
||||
}
|
||||
// nginx -t écrit sur stderr même en cas de succès
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("nginx config invalid: %s", result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReloadNginx recharge la configuration Nginx.
|
||||
func (c *SSHClient) ReloadNginx(ctx context.Context) error {
|
||||
// D'abord tester la config
|
||||
if err := c.TestNginxConfig(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := c.Exec(ctx, "systemctl reload nginx")
|
||||
if err != nil {
|
||||
return fmt.Errorf("nginx reload: %w", err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("nginx reload failed: %s", result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestartNginx redémarre Nginx.
|
||||
func (c *SSHClient) RestartNginx(ctx context.Context) error {
|
||||
result, err := c.Exec(ctx, "systemctl restart nginx")
|
||||
if err != nil {
|
||||
return fmt.Errorf("nginx restart: %w", err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("nginx restart failed: %s", result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NginxStatus retourne le statut de Nginx.
|
||||
func (c *SSHClient) NginxStatus(ctx context.Context) (string, error) {
|
||||
result, err := c.Exec(ctx, "systemctl is-active nginx")
|
||||
if err != nil {
|
||||
return "unknown", nil
|
||||
}
|
||||
return strings.TrimSpace(result.Stdout), nil
|
||||
}
|
||||
|
||||
// ListNginxSites liste les sites Nginx configurés.
|
||||
func (c *SSHClient) ListNginxSites(ctx context.Context) ([]NginxSiteInfo, error) {
|
||||
// Lister sites-available
|
||||
result, err := c.Exec(ctx, fmt.Sprintf("ls -1 %s", NginxSitesAvailable))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list sites-available: %w", err)
|
||||
}
|
||||
|
||||
if result.ExitCode != 0 {
|
||||
return nil, fmt.Errorf("list sites-available failed: %s", result.Stderr)
|
||||
}
|
||||
|
||||
// Lister sites-enabled
|
||||
enabledResult, err := c.Exec(ctx, fmt.Sprintf("ls -1 %s", NginxSitesEnabled))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list sites-enabled: %w", err)
|
||||
}
|
||||
|
||||
enabledSet := make(map[string]bool)
|
||||
for _, name := range strings.Split(enabledResult.Stdout, "\n") {
|
||||
name = strings.TrimSpace(name)
|
||||
if name != "" {
|
||||
enabledSet[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
var sites []NginxSiteInfo
|
||||
for _, name := range strings.Split(result.Stdout, "\n") {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" || name == "default" {
|
||||
continue
|
||||
}
|
||||
|
||||
site := NginxSiteInfo{
|
||||
Name: name,
|
||||
Enabled: enabledSet[name],
|
||||
}
|
||||
sites = append(sites, site)
|
||||
}
|
||||
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
// GetNginxSiteConfig récupère la config d'un site.
|
||||
func (c *SSHClient) GetNginxSiteConfig(ctx context.Context, name string) (string, error) {
|
||||
path := filepath.Join(NginxSitesAvailable, name)
|
||||
content, err := c.ReadFile(ctx, path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read site config: %w", err)
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
// WriteNginxSiteConfig écrit la config d'un site.
|
||||
func (c *SSHClient) WriteNginxSiteConfig(ctx context.Context, name string, config string) error {
|
||||
path := filepath.Join(NginxSitesAvailable, name)
|
||||
return c.WriteFile(ctx, path, []byte(config), "644")
|
||||
}
|
||||
|
||||
// EnableNginxSite active un site (crée le lien symbolique).
|
||||
func (c *SSHClient) EnableNginxSite(ctx context.Context, name string) error {
|
||||
src := filepath.Join(NginxSitesAvailable, name)
|
||||
dst := filepath.Join(NginxSitesEnabled, name)
|
||||
|
||||
// Vérifier que le site existe
|
||||
exists, err := c.FileExists(ctx, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return fmt.Errorf("site %s does not exist", name)
|
||||
}
|
||||
|
||||
// Créer le lien
|
||||
result, err := c.Exec(ctx, fmt.Sprintf("ln -sf %s %s", src, dst))
|
||||
if err != nil {
|
||||
return fmt.Errorf("enable site: %w", err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("enable site failed: %s", result.Stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableNginxSite désactive un site (supprime le lien symbolique).
|
||||
func (c *SSHClient) DisableNginxSite(ctx context.Context, name string) error {
|
||||
path := filepath.Join(NginxSitesEnabled, name)
|
||||
|
||||
result, err := c.Exec(ctx, fmt.Sprintf("rm -f %s", path))
|
||||
if err != nil {
|
||||
return fmt.Errorf("disable site: %w", err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("disable site failed: %s", result.Stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteNginxSite supprime un site (désactive puis supprime).
|
||||
func (c *SSHClient) DeleteNginxSite(ctx context.Context, name string) error {
|
||||
// D'abord désactiver
|
||||
if err := c.DisableNginxSite(ctx, name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Puis supprimer
|
||||
path := filepath.Join(NginxSitesAvailable, name)
|
||||
result, err := c.Exec(ctx, fmt.Sprintf("rm -f %s", path))
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete site: %w", err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("delete site failed: %s", result.Stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeployNginxSite déploie un site complet (écrire, activer, recharger).
|
||||
func (c *SSHClient) DeployNginxSite(ctx context.Context, name string, config string) error {
|
||||
// Écrire la config
|
||||
if err := c.WriteNginxSiteConfig(ctx, name, config); err != nil {
|
||||
return fmt.Errorf("write config: %w", err)
|
||||
}
|
||||
|
||||
// Activer le site
|
||||
if err := c.EnableNginxSite(ctx, name); err != nil {
|
||||
return fmt.Errorf("enable site: %w", err)
|
||||
}
|
||||
|
||||
// Recharger Nginx
|
||||
if err := c.ReloadNginx(ctx); err != nil {
|
||||
// En cas d'erreur, désactiver le site
|
||||
c.DisableNginxSite(ctx, name)
|
||||
return fmt.Errorf("reload nginx: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateNginxProxyConfig génère une config proxy standard.
|
||||
func GenerateNginxProxyConfig(domain, upstream string, ssl bool) string {
|
||||
var config strings.Builder
|
||||
|
||||
if ssl {
|
||||
// Redirect HTTP to HTTPS
|
||||
config.WriteString(fmt.Sprintf(`server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name %s;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
`, domain))
|
||||
}
|
||||
|
||||
// Main server block
|
||||
if ssl {
|
||||
config.WriteString(fmt.Sprintf(`server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name %s;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/%s/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/%s/privkey.pem;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
location / {
|
||||
proxy_pass %s;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
}
|
||||
`, domain, domain, domain, upstream))
|
||||
} else {
|
||||
config.WriteString(fmt.Sprintf(`server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name %s;
|
||||
|
||||
location / {
|
||||
proxy_pass %s;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
}
|
||||
`, domain, upstream))
|
||||
}
|
||||
|
||||
return config.String()
|
||||
}
|
||||
|
||||
// RequestSSLCertificate demande un certificat Let's Encrypt.
|
||||
func (c *SSHClient) RequestSSLCertificate(ctx context.Context, domain, email string) error {
|
||||
cmd := fmt.Sprintf("certbot certonly --nginx -d %s --non-interactive --agree-tos -m %s", domain, email)
|
||||
result, err := c.Exec(ctx, cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("certbot: %w", err)
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("certbot failed: %s", result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckSSLCertificate vérifie si un certificat SSL existe.
|
||||
func (c *SSHClient) CheckSSLCertificate(ctx context.Context, domain string) (bool, error) {
|
||||
path := fmt.Sprintf("/etc/letsencrypt/live/%s/fullchain.pem", domain)
|
||||
return c.FileExists(ctx, path)
|
||||
}
|
||||
Reference in New Issue
Block a user