Files
sogoms/cmd/sogoms/admin/services.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

962 lines
23 KiB
Go

package main
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"net"
"os"
"path/filepath"
"sort"
"strings"
"time"
"gopkg.in/yaml.v3"
"sogoms.com/internal/protocol"
)
// ServicePool centralise les connexions vers les microservices.
type ServicePool struct {
DB *protocol.Pool
Logs *protocol.Pool
Cron *protocol.Pool
}
// ServiceStatus représente le statut d'un service.
type ServiceStatus struct {
Name string `json:"name"`
Available bool `json:"available"`
LatencyMs int64 `json:"latency_ms"`
}
// HealthCheck vérifie l'état de tous les services.
func (sp *ServicePool) HealthCheck() []ServiceStatus {
statuses := make([]ServiceStatus, 0, 3)
// Check sogoms-db
if sp.DB != nil {
statuses = append(statuses, sp.checkService("sogoms-db", sp.DB))
}
// Check sogoms-logs
if sp.Logs != nil {
statuses = append(statuses, sp.checkService("sogoms-logs", sp.Logs))
}
// Check sogoms-cron
if sp.Cron != nil {
statuses = append(statuses, sp.checkService("sogoms-cron", sp.Cron))
}
return statuses
}
// checkService vérifie un service individuel.
func (sp *ServicePool) checkService(name string, pool *protocol.Pool) ServiceStatus {
status := ServiceStatus{
Name: name,
Available: false,
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
start := time.Now()
req := protocol.NewRequest("health", nil)
resp, err := pool.Call(ctx, req)
status.LatencyMs = time.Since(start).Milliseconds()
if err == nil && resp != nil && resp.Status == "success" {
status.Available = true
}
return status
}
// GetCronJobs récupère la liste des jobs cron.
func (sp *ServicePool) GetCronJobs() ([]map[string]any, error) {
if sp.Cron == nil {
return nil, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req := protocol.NewRequest("list", nil)
resp, err := sp.Cron.Call(ctx, req)
if err != nil {
return nil, err
}
if resp.Status != "success" {
return nil, nil
}
// Extraire les jobs
if result, ok := resp.Result.(map[string]any); ok {
if jobs, ok := result["jobs"].([]any); ok {
jobList := make([]map[string]any, 0, len(jobs))
for _, j := range jobs {
if job, ok := j.(map[string]any); ok {
jobList = append(jobList, job)
}
}
return jobList, nil
}
}
return nil, nil
}
// TriggerCronJob déclenche un job cron manuellement.
func (sp *ServicePool) TriggerCronJob(appID, jobName string) error {
if sp.Cron == nil {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req := protocol.NewRequest("trigger", map[string]any{
"app_id": appID,
"job": jobName,
})
_, err := sp.Cron.Call(ctx, req)
return err
}
// GetCronHistory récupère l'historique des exécutions cron.
func (sp *ServicePool) GetCronHistory(appID string, limit int) ([]map[string]any, error) {
if sp.Cron == nil {
return nil, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
params := map[string]any{"limit": limit}
if appID != "" {
params["app_id"] = appID
}
req := protocol.NewRequest("status", params)
resp, err := sp.Cron.Call(ctx, req)
if err != nil {
return nil, err
}
if resp.Status != "success" {
return nil, nil
}
// Extraire les exécutions
if result, ok := resp.Result.(map[string]any); ok {
if execs, ok := result["executions"].([]any); ok {
execList := make([]map[string]any, 0, len(execs))
for _, e := range execs {
if exec, ok := e.(map[string]any); ok {
execList = append(execList, exec)
}
}
return execList, nil
}
}
return nil, nil
}
// CreateApp crée une nouvelle application avec sa configuration.
func (sp *ServicePool) CreateApp(appName, version, basePath string, hosts []string, dbHost, dbPort, dbUser, dbName, dbPassword, jwtExpiry string) error {
configDir := "/config"
secretsDir := "/secrets"
// Créer le dossier de l'app
appDir := filepath.Join(configDir, "apps", appName)
if err := os.MkdirAll(appDir, 0755); err != nil {
return fmt.Errorf("mkdir app: %w", err)
}
// Générer le JWT secret
jwtSecret := make([]byte, 32)
if _, err := rand.Read(jwtSecret); err != nil {
return fmt.Errorf("generate jwt secret: %w", err)
}
jwtSecretB64 := base64.StdEncoding.EncodeToString(jwtSecret)
// Créer les fichiers secrets
dbPassFile := filepath.Join(secretsDir, appName+"_db_pass")
if err := os.WriteFile(dbPassFile, []byte(dbPassword), 0600); err != nil {
return fmt.Errorf("write db password: %w", err)
}
jwtSecretFile := filepath.Join(secretsDir, appName+"_jwt_secret")
if err := os.WriteFile(jwtSecretFile, []byte(jwtSecretB64), 0600); err != nil {
return fmt.Errorf("write jwt secret: %w", err)
}
// Générer app.yaml
hostsYAML := ""
for _, h := range hosts {
hostsYAML += fmt.Sprintf(" - %s\n", h)
}
appYAML := fmt.Sprintf(`# Application %s
# Générée automatiquement par sogoms-admin
app: %s
version: "%s"
base_path: %s
hosts:
%s
database:
host: %s
port: %s
user: %s
password_file: %s
name: %s
auth:
jwt_secret_file: %s
jwt_expiry: %s
logs:
retention_days: 30
routes: []
`, appName, appName, version, basePath, hostsYAML, dbHost, dbPort, dbUser, dbPassFile, dbName, jwtSecretFile, jwtExpiry)
appYAMLFile := filepath.Join(appDir, "app.yaml")
if err := os.WriteFile(appYAMLFile, []byte(appYAML), 0644); err != nil {
return fmt.Errorf("write app.yaml: %w", err)
}
return nil
}
// ScanAndGenerateSchema introspect la DB et génère schema.yaml.
// Retourne le nombre de tables détectées.
func (sp *ServicePool) ScanAndGenerateSchema(appID string) (int, error) {
if sp.DB == nil {
return 0, fmt.Errorf("db service not available")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Appeler l'introspection
req := protocol.NewRequest("introspect", map[string]any{
"app_id": appID,
})
resp, err := sp.DB.Call(ctx, req)
if err != nil {
return 0, fmt.Errorf("introspect call: %w", err)
}
if resp.Status != "success" {
if resp.Error != nil {
return 0, fmt.Errorf("introspect failed: %s", resp.Error.Message)
}
return 0, fmt.Errorf("introspect failed")
}
// Extraire les tables du résultat
result, ok := resp.Result.(map[string]any)
if !ok {
return 0, fmt.Errorf("invalid introspect result")
}
tablesRaw, ok := result["tables"].(map[string]any)
if !ok {
return 0, fmt.Errorf("no tables in result")
}
tableCount := len(tablesRaw)
// Construire le schema
schema := map[string]any{
"app": appID,
"tables": convertTablesToSchema(tablesRaw),
}
// Sérialiser en YAML
yamlData, err := yaml.Marshal(schema)
if err != nil {
return 0, fmt.Errorf("yaml marshal: %w", err)
}
// Écrire le fichier
schemaFile := filepath.Join("/config", "apps", appID, "schema.yaml")
if err := os.WriteFile(schemaFile, yamlData, 0644); err != nil {
return 0, fmt.Errorf("write schema.yaml: %w", err)
}
return tableCount, nil
}
// convertTablesToSchema convertit les données d'introspection en format schema.
func convertTablesToSchema(tablesRaw map[string]any) map[string]any {
tables := make(map[string]any)
// Trier les tables par nom pour un output cohérent
tableNames := make([]string, 0, len(tablesRaw))
for name := range tablesRaw {
tableNames = append(tableNames, name)
}
sort.Strings(tableNames)
// Première passe : créer toutes les tables
for _, tableName := range tableNames {
tableData, ok := tablesRaw[tableName].(map[string]any)
if !ok {
continue
}
table := make(map[string]any)
// Colonnes
if columnsRaw, ok := tableData["columns"].(map[string]any); ok {
columns := make(map[string]any)
// Trier les colonnes
colNames := make([]string, 0, len(columnsRaw))
for name := range columnsRaw {
colNames = append(colNames, name)
}
sort.Strings(colNames)
for _, colName := range colNames {
colData, ok := columnsRaw[colName].(map[string]any)
if !ok {
continue
}
col := make(map[string]any)
// Type
if t, ok := colData["type"].(string); ok {
col["type"] = t
}
// Longueur
if l, ok := colData["length"].(float64); ok && l > 0 {
col["length"] = int(l)
}
// Primary
if p, ok := colData["primary"].(bool); ok && p {
col["primary"] = true
}
// Auto increment
if a, ok := colData["auto"].(bool); ok && a {
col["auto"] = true
}
// Required (NOT NULL)
if r, ok := colData["required"].(bool); ok && r {
col["required"] = true
}
// Default
if d, ok := colData["default"].(string); ok && d != "" {
col["default"] = d
}
// Unique
if u, ok := colData["unique"].(bool); ok && u {
col["unique"] = true
}
// Foreign key
if fk, ok := colData["foreign"].(string); ok && fk != "" {
col["foreign"] = fk
}
// Détecter filter: owner pour les colonnes user_id
if colName == "user_id" {
col["filter"] = "owner"
}
columns[colName] = col
}
table["columns"] = columns
}
// Primary keys
if pk, ok := tableData["primary"].([]any); ok && len(pk) > 0 {
pkStrings := make([]string, 0, len(pk))
for _, p := range pk {
if s, ok := p.(string); ok {
pkStrings = append(pkStrings, s)
}
}
table["primary"] = pkStrings
}
// Détecter soft_delete (colonne deleted_at)
if hasSoftDelete(tableData) {
table["soft_delete"] = true
}
// CRUD par défaut (sauf tables de liaison)
if hasUserID(tableData) {
table["crud"] = []string{"list", "show", "create", "update", "delete"}
} else {
table["crud"] = []string{}
}
tables[tableName] = table
}
// Deuxième passe : détecter cascade sur les tables parent
// Une table parent a cascade si elle a soft_delete ET des tables enfants avec soft_delete
for parentName, parentTable := range tables {
parent, ok := parentTable.(map[string]any)
if !ok {
continue
}
// Si la table parent n'a pas soft_delete, pas de cascade
if sd, ok := parent["soft_delete"].(bool); !ok || !sd {
continue
}
// Chercher si des tables enfants ont une FK vers cette table
hasChildWithSoftDelete := false
for childName, childTable := range tables {
if childName == parentName {
continue
}
child, ok := childTable.(map[string]any)
if !ok {
continue
}
// Vérifier si l'enfant a soft_delete
childSD, _ := child["soft_delete"].(bool)
if !childSD {
continue
}
// Vérifier si l'enfant a une FK vers le parent
if cols, ok := child["columns"].(map[string]any); ok {
for _, colData := range cols {
if col, ok := colData.(map[string]any); ok {
if fk, ok := col["foreign"].(string); ok {
// fk = "table.column"
if strings.HasPrefix(fk, parentName+".") {
hasChildWithSoftDelete = true
break
}
}
}
}
}
if hasChildWithSoftDelete {
break
}
}
if hasChildWithSoftDelete {
parent["cascade"] = true
}
}
return tables
}
// hasUserID vérifie si une table a une colonne user_id.
func hasUserID(tableData map[string]any) bool {
if columnsRaw, ok := tableData["columns"].(map[string]any); ok {
_, hasIt := columnsRaw["user_id"]
return hasIt
}
return false
}
// hasSoftDelete vérifie si une table a une colonne deleted_at (TIMESTAMP/DATETIME).
func hasSoftDelete(tableData map[string]any) bool {
if columnsRaw, ok := tableData["columns"].(map[string]any); ok {
if col, ok := columnsRaw["deleted_at"].(map[string]any); ok {
if colType, ok := col["type"].(string); ok && colType == "datetime" {
return true
}
}
}
return false
}
// UpdateLoginData met à jour le bloc login_data dans auth.yaml
// en se basant sur le schema généré (tables avec filter: owner).
func UpdateLoginData(appID string) error {
// 1. Lire le schema.yaml
schemaPath := filepath.Join("/config", "apps", appID, "schema.yaml")
schemaData, err := os.ReadFile(schemaPath)
if err != nil {
return fmt.Errorf("read schema: %w", err)
}
var schema map[string]any
if err := yaml.Unmarshal(schemaData, &schema); err != nil {
return fmt.Errorf("parse schema: %w", err)
}
// 2. Identifier les tables avec user_id (filter: owner)
tablesRaw, ok := schema["tables"].(map[string]any)
if !ok {
return nil // Pas de tables, rien à faire
}
loginData := make(map[string]string)
tableNames := make([]string, 0, len(tablesRaw))
for name := range tablesRaw {
tableNames = append(tableNames, name)
}
sort.Strings(tableNames)
for _, tableName := range tableNames {
tableRaw := tablesRaw[tableName]
table, ok := tableRaw.(map[string]any)
if !ok {
continue
}
// Vérifier si la table a une colonne avec filter: owner
columns, ok := table["columns"].(map[string]any)
if !ok {
continue
}
hasOwnerFilter := false
for _, colRaw := range columns {
col, ok := colRaw.(map[string]any)
if !ok {
continue
}
if filter, ok := col["filter"].(string); ok && filter == "owner" {
hasOwnerFilter = true
break
}
}
if !hasOwnerFilter {
continue
}
// Collecter les noms de colonnes (sauf user_id)
colNames := make([]string, 0, len(columns))
hasPosition := false
for colName := range columns {
if colName == "user_id" {
continue // On n'inclut pas user_id dans le SELECT
}
colNames = append(colNames, colName)
if colName == "position" {
hasPosition = true
}
}
sort.Strings(colNames)
// Mettre id en premier si présent
for i, name := range colNames {
if name == "id" {
colNames = append([]string{"id"}, append(colNames[:i], colNames[i+1:]...)...)
break
}
}
// Construire la requête
query := fmt.Sprintf("SELECT %s\nFROM %s WHERE user_id = ?",
strings.Join(colNames, ", "), tableName)
// Ajouter ORDER BY si position existe
if hasPosition {
query += " ORDER BY position"
}
loginData[tableName] = query
}
if len(loginData) == 0 {
return nil // Pas de tables owner, rien à générer
}
// 3. Lire auth.yaml existant
authPath := filepath.Join("/config", "apps", appID, "queries", "auth.yaml")
var existingData map[string]any
if data, err := os.ReadFile(authPath); err == nil {
if err := yaml.Unmarshal(data, &existingData); err != nil {
existingData = make(map[string]any)
}
} else {
existingData = make(map[string]any)
}
// 4. Mettre à jour seulement login_data
existingData["login_data"] = loginData
// 5. Réécrire le fichier avec commentaire
queriesDir := filepath.Dir(authPath)
if err := os.MkdirAll(queriesDir, 0755); err != nil {
return fmt.Errorf("create queries dir: %w", err)
}
yamlData, err := yaml.Marshal(existingData)
if err != nil {
return fmt.Errorf("marshal auth.yaml: %w", err)
}
// Ajouter un header
header := "# Requêtes d'authentification\n# login_data généré automatiquement depuis schema.yaml\n\n"
finalData := []byte(header + string(yamlData))
if err := os.WriteFile(authPath, finalData, 0644); err != nil {
return fmt.Errorf("write auth.yaml: %w", err)
}
return nil
}
// GenerateRoutesFromSchema génère les routes CRUD dans app.yaml basées sur schema.yaml.
func GenerateRoutesFromSchema(appID string) error {
// 1. Lire le schema.yaml
schemaPath := filepath.Join("/config", "apps", appID, "schema.yaml")
schemaData, err := os.ReadFile(schemaPath)
if err != nil {
return fmt.Errorf("read schema: %w", err)
}
var schema map[string]any
if err := yaml.Unmarshal(schemaData, &schema); err != nil {
return fmt.Errorf("parse schema: %w", err)
}
// 2. Lire app.yaml existant
appPath := filepath.Join("/config", "apps", appID, "app.yaml")
appData, err := os.ReadFile(appPath)
if err != nil {
return fmt.Errorf("read app.yaml: %w", err)
}
var appConfig map[string]any
if err := yaml.Unmarshal(appData, &appConfig); err != nil {
return fmt.Errorf("parse app.yaml: %w", err)
}
// 3. Extraire les tables avec CRUD
tablesRaw, ok := schema["tables"].(map[string]any)
if !ok {
return nil // Pas de tables
}
// Trier les tables
tableNames := make([]string, 0, len(tablesRaw))
for name := range tablesRaw {
tableNames = append(tableNames, name)
}
sort.Strings(tableNames)
// 4. Générer les routes
routes := []map[string]any{
// Routes auth par défaut
{"path": "/auth/register", "method": "POST", "scenario": appID + "/auth/register", "auth": false},
{"path": "/auth/login", "method": "POST", "scenario": appID + "/auth/login", "auth": false},
{"path": "/auth/logout", "method": "POST", "scenario": appID + "/auth/logout"},
{"path": "/auth/me", "method": "GET", "scenario": appID + "/auth/me"},
}
for _, tableName := range tableNames {
tableRaw := tablesRaw[tableName]
table, ok := tableRaw.(map[string]any)
if !ok {
continue
}
// Vérifier si CRUD est défini
crudRaw, ok := table["crud"].([]any)
if !ok || len(crudRaw) == 0 {
continue
}
// Convertir en map pour lookup rapide
crudOps := make(map[string]bool)
for _, op := range crudRaw {
if opStr, ok := op.(string); ok {
crudOps[opStr] = true
}
}
// Générer les routes pour cette table
if crudOps["list"] {
routes = append(routes, map[string]any{
"path": "/" + tableName,
"method": "GET",
"scenario": appID + "/" + tableName + "/list",
})
}
if crudOps["create"] {
routes = append(routes, map[string]any{
"path": "/" + tableName,
"method": "POST",
"scenario": appID + "/" + tableName + "/create",
})
}
if crudOps["show"] {
routes = append(routes, map[string]any{
"path": "/" + tableName + "/{id}",
"method": "GET",
"scenario": appID + "/" + tableName + "/show",
})
}
if crudOps["update"] {
routes = append(routes, map[string]any{
"path": "/" + tableName + "/{id}",
"method": "PUT",
"scenario": appID + "/" + tableName + "/update",
})
}
if crudOps["delete"] {
routes = append(routes, map[string]any{
"path": "/" + tableName + "/{id}",
"method": "DELETE",
"scenario": appID + "/" + tableName + "/delete",
})
}
}
// 5. Mettre à jour app.yaml
appConfig["routes"] = routes
// 6. Réécrire le fichier
yamlData, err := yaml.Marshal(appConfig)
if err != nil {
return fmt.Errorf("marshal app.yaml: %w", err)
}
header := fmt.Sprintf("# Application %s\n# Routes générées automatiquement depuis schema.yaml\n\n", appID)
if err := os.WriteFile(appPath, []byte(header+string(yamlData)), 0644); err != nil {
return fmt.Errorf("write app.yaml: %w", err)
}
return nil
}
// GenerateQueriesFromSchema génère les fichiers queries/*.yaml basés sur schema.yaml.
func GenerateQueriesFromSchema(appID string) error {
// 1. Lire le schema.yaml
schemaPath := filepath.Join("/config", "apps", appID, "schema.yaml")
schemaData, err := os.ReadFile(schemaPath)
if err != nil {
return fmt.Errorf("read schema: %w", err)
}
var schema map[string]any
if err := yaml.Unmarshal(schemaData, &schema); err != nil {
return fmt.Errorf("parse schema: %w", err)
}
// 2. Créer le dossier queries
queriesDir := filepath.Join("/config", "apps", appID, "queries")
if err := os.MkdirAll(queriesDir, 0755); err != nil {
return fmt.Errorf("create queries dir: %w", err)
}
// 3. Extraire les tables
tablesRaw, ok := schema["tables"].(map[string]any)
if !ok {
return nil
}
for tableName, tableRaw := range tablesRaw {
table, ok := tableRaw.(map[string]any)
if !ok {
continue
}
// Vérifier si CRUD est défini
crudRaw, ok := table["crud"].([]any)
if !ok || len(crudRaw) == 0 {
continue
}
// Générer le fichier queries pour cette table
if err := generateTableQueries(queriesDir, tableName, table); err != nil {
return fmt.Errorf("generate queries for %s: %w", tableName, err)
}
}
return nil
}
// generateTableQueries génère le fichier queries pour une table.
func generateTableQueries(queriesDir, tableName string, table map[string]any) error {
columns, ok := table["columns"].(map[string]any)
if !ok {
return nil
}
// Collecter les colonnes
colNames := make([]string, 0, len(columns))
createFields := make([]string, 0, len(columns))
updateFields := make([]string, 0, len(columns))
hasPosition := false
hasUserID := false
for colName, colRaw := range columns {
col, ok := colRaw.(map[string]any)
if !ok {
continue
}
colNames = append(colNames, colName)
if colName == "position" {
hasPosition = true
}
if colName == "user_id" {
hasUserID = true
}
// Exclure les colonnes auto-générées du CREATE
isAuto, _ := col["auto"].(bool)
isPrimary, _ := col["primary"].(bool)
isAutoGenerated := colName == "created_at" || colName == "updated_at" || colName == "deleted_at"
if !isAuto && !isAutoGenerated {
createFields = append(createFields, colName)
}
// Exclure id, user_id et auto-générées de l'UPDATE
if !isPrimary && !isAuto && !isAutoGenerated && colName != "user_id" {
updateFields = append(updateFields, colName)
}
}
sort.Strings(colNames)
sort.Strings(createFields)
sort.Strings(updateFields)
// Mettre id en premier dans les colonnes SELECT
selectCols := make([]string, 0, len(colNames))
for _, name := range colNames {
if name == "id" {
selectCols = append([]string{"id"}, selectCols...)
} else if name != "user_id" { // Exclure user_id du SELECT
selectCols = append(selectCols, name)
}
}
// Construire le contenu YAML
queries := make(map[string]any)
// LIST
listQuery := fmt.Sprintf("SELECT %s\nFROM %s", strings.Join(selectCols, ", "), tableName)
orderBy := ""
if hasPosition {
orderBy = "position ASC"
}
listConfig := map[string]any{
"query": listQuery,
}
if hasUserID {
listConfig["filters"] = map[string]string{
"default": "user_id = :user_id",
"admin": "",
}
}
if orderBy != "" {
listConfig["order"] = orderBy
}
queries["list"] = listConfig
// SHOW
showQuery := fmt.Sprintf("SELECT %s\nFROM %s WHERE id = :id", strings.Join(selectCols, ", "), tableName)
showConfig := map[string]any{
"query": showQuery,
}
if hasUserID {
showConfig["filters"] = map[string]string{
"default": "user_id = :user_id",
"admin": "",
}
}
queries["show"] = showConfig
// CREATE
queries["create"] = map[string]any{
"table": tableName,
"fields": createFields,
}
// UPDATE
updateConfig := map[string]any{
"table": tableName,
"fields": updateFields,
}
if hasUserID {
updateConfig["filters"] = map[string]string{
"default": "user_id = :user_id",
"admin": "",
}
}
queries["update"] = updateConfig
// DELETE
deleteConfig := map[string]any{
"table": tableName,
}
if hasUserID {
deleteConfig["filters"] = map[string]string{
"default": "user_id = :user_id",
"admin": "",
}
}
queries["delete"] = deleteConfig
// Sérialiser
yamlData, err := yaml.Marshal(queries)
if err != nil {
return err
}
header := fmt.Sprintf("# Requêtes CRUD %s\n# Généré automatiquement depuis schema.yaml\n\n", tableName)
queryFile := filepath.Join(queriesDir, tableName+".yaml")
return os.WriteFile(queryFile, []byte(header+string(yamlData)), 0644)
}
// ReloadGateway demande à sogoctl de recharger sogoway.
func (sp *ServicePool) ReloadGateway() error {
conn, err := net.DialTimeout("unix", "/run/sogoctl.sock", 2*time.Second)
if err != nil {
return fmt.Errorf("connect to sogoctl: %w", err)
}
defer conn.Close()
// Envoyer la commande reload
_, err = conn.Write([]byte("reload sogoway\n"))
if err != nil {
return fmt.Errorf("send command: %w", err)
}
// Lire la réponse
buf := make([]byte, 256)
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
n, err := conn.Read(buf)
if err != nil {
return fmt.Errorf("read response: %w", err)
}
response := string(buf[:n])
if strings.HasPrefix(response, "error:") {
return fmt.Errorf("%s", strings.TrimPrefix(response, "error: "))
}
return nil
}