Files
sogoms/internal/config/config.go
Pierre a4694a10d1 SOGOMS v1.0.1 - Microservices logs, smtp et roadmap
Nouveaux services:
- sogoms-logs : logging centralisé avec rotation
- sogoms-smtp : envoi emails avec templates YAML

Nouvelles fonctionnalités:
- Queries YAML externalisées (config/queries/{app}/)
- CRUD générique paramétrable
- Filtres par rôle (default, admin)
- Templates email (config/emails/{app}/)

Documentation:
- DOCTECH.md : documentation technique complète
- README.md : vision et roadmap
- TODO.md : phases 11-15 planifiées

Roadmap:
- Phase 11: sogoms-crypt (chiffrement)
- Phase 12: sogoms-imap/mailproc (emails)
- Phase 13: sogoms-cron (tâches planifiées)
- Phase 14: sogoms-push (MQTT temps réel)
- Phase 15: sogoms-schema (API auto-générée)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 14:58:46 +01:00

443 lines
10 KiB
Go

// Package config gère le chargement des configurations YAML.
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"gopkg.in/yaml.v3"
)
// AppConfig représente la configuration d'une application cliente.
type AppConfig struct {
App string `yaml:"app"`
Version string `yaml:"version"`
BasePath string `yaml:"base_path"`
Hosts []string `yaml:"hosts"`
Database Database `yaml:"database"`
Auth Auth `yaml:"auth"`
Routes []Route `yaml:"routes"`
Queries *Queries // Chargé depuis config/queries/{app}/
}
// Queries stocke les requêtes SQL par domaine.
type Queries struct {
Auth map[string]any `yaml:",inline"`
Projects map[string]any `yaml:",inline"`
Tasks map[string]any `yaml:",inline"`
Tags map[string]any `yaml:",inline"`
Statuses map[string]any `yaml:",inline"`
files map[string]map[string]any // domaine -> clé -> requête
}
// Get retourne une requête par domaine et clé.
func (q *Queries) Get(domain, key string) string {
if q == nil || q.files == nil {
return ""
}
if domainMap, ok := q.files[domain]; ok {
if val, ok := domainMap[key]; ok {
if s, ok := val.(string); ok {
return strings.TrimSpace(s)
}
}
}
return ""
}
// GetMap retourne une map de requêtes (ex: login_data).
func (q *Queries) GetMap(domain, key string) map[string]string {
if q == nil || q.files == nil {
return nil
}
if domainMap, ok := q.files[domain]; ok {
if val, ok := domainMap[key]; ok {
if m, ok := val.(map[string]any); ok {
result := make(map[string]string)
for k, v := range m {
if s, ok := v.(string); ok {
result[k] = strings.TrimSpace(s)
}
}
return result
}
}
}
return nil
}
// QueryConfig représente une requête paramétrable.
type QueryConfig struct {
Query string
Filters map[string]string
Order string
}
// GetQuery retourne une QueryConfig pour un domaine et une clé.
func (q *Queries) GetQuery(domain, key string) *QueryConfig {
if q == nil || q.files == nil {
return nil
}
domainMap, ok := q.files[domain]
if !ok {
return nil
}
val, ok := domainMap[key]
if !ok {
return nil
}
m, ok := val.(map[string]any)
if !ok {
// Ancienne syntaxe : juste une string
if s, ok := val.(string); ok {
return &QueryConfig{Query: strings.TrimSpace(s)}
}
return nil
}
qc := &QueryConfig{
Filters: make(map[string]string),
}
if query, ok := m["query"].(string); ok {
qc.Query = strings.TrimSpace(query)
}
if order, ok := m["order"].(string); ok {
qc.Order = strings.TrimSpace(order)
}
if filters, ok := m["filters"].(map[string]any); ok {
for k, v := range filters {
if s, ok := v.(string); ok {
qc.Filters[k] = strings.TrimSpace(s)
}
}
}
return qc
}
// CUDConfig représente une config pour Create/Update/Delete.
type CUDConfig struct {
Table string
Fields []string
Filters map[string]string
}
// GetCUD retourne une CUDConfig pour un domaine et une clé (create/update/delete).
func (q *Queries) GetCUD(domain, key string) *CUDConfig {
if q == nil || q.files == nil {
return nil
}
domainMap, ok := q.files[domain]
if !ok {
return nil
}
val, ok := domainMap[key]
if !ok {
return nil
}
m, ok := val.(map[string]any)
if !ok {
return nil
}
cud := &CUDConfig{
Filters: make(map[string]string),
}
if table, ok := m["table"].(string); ok {
cud.Table = table
}
if fields, ok := m["fields"].([]any); ok {
for _, f := range fields {
if s, ok := f.(string); ok {
cud.Fields = append(cud.Fields, s)
}
}
}
if filters, ok := m["filters"].(map[string]any); ok {
for k, v := range filters {
if s, ok := v.(string); ok {
cud.Filters[k] = strings.TrimSpace(s)
}
}
}
return cud
}
// GetFilter retourne le filtre pour un rôle donné.
func (cud *CUDConfig) GetFilter(role string) string {
if cud == nil {
return ""
}
if f, ok := cud.Filters[role]; ok {
return f
}
if f, ok := cud.Filters["default"]; ok {
return f
}
return ""
}
// Build construit la requête SQL finale avec filtres et ordre.
// role: rôle de l'utilisateur (ou "default")
// params: map de placeholders à remplacer (:user_id, :id, etc.)
func (qc *QueryConfig) Build(role string, params map[string]any) (string, []any) {
if qc == nil {
return "", nil
}
query := qc.Query
// Déterminer le filtre à appliquer
filter := ""
if f, ok := qc.Filters[role]; ok {
filter = f
} else if f, ok := qc.Filters["default"]; ok {
filter = f
}
// Construire la requête
hasWhere := strings.Contains(strings.ToUpper(query), " WHERE ")
if filter != "" {
if hasWhere {
query += " AND " + filter
} else {
query += " WHERE " + filter
}
}
if qc.Order != "" {
query += " ORDER BY " + qc.Order
}
// Remplacer les placeholders :name par ? et collecter les args
var args []any
for {
idx := strings.Index(query, ":")
if idx == -1 {
break
}
// Trouver la fin du placeholder
end := idx + 1
for end < len(query) && (query[end] == '_' || (query[end] >= 'a' && query[end] <= 'z') || (query[end] >= 'A' && query[end] <= 'Z') || (query[end] >= '0' && query[end] <= '9')) {
end++
}
placeholder := query[idx+1 : end]
if val, ok := params[placeholder]; ok {
args = append(args, val)
query = query[:idx] + "?" + query[end:]
} else {
// Placeholder non trouvé, on laisse tel quel (peut être une erreur)
break
}
}
return query, args
}
// Auth contient la configuration d'authentification.
type Auth struct {
JWTSecretFile string `yaml:"jwt_secret_file"`
JWTExpiry string `yaml:"jwt_expiry"`
jwtSecret string // Chargé depuis le fichier
}
// JWTSecret retourne le secret JWT (chargé depuis le fichier).
func (a *Auth) JWTSecret() string {
return a.jwtSecret
}
// Database contient la configuration de connexion à la base de données.
type Database struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
PasswordFile string `yaml:"password_file"`
Name string `yaml:"name"`
password string // Chargé depuis le fichier
}
// Password retourne le mot de passe (chargé depuis le fichier).
func (d *Database) Password() string {
return d.password
}
// DSN retourne la chaîne de connexion MySQL/MariaDB.
func (d *Database) DSN() string {
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&charset=utf8mb4",
d.User, d.password, d.Host, d.Port, d.Name)
}
// Route représente une route API.
type Route struct {
Path string `yaml:"path"`
Method string `yaml:"method"`
Scenario string `yaml:"scenario"`
Auth *bool `yaml:"auth,omitempty"`
}
// Registry stocke les configurations des applications.
type Registry struct {
configDir string
apps map[string]*AppConfig // Par app_id
byHost map[string]*AppConfig // Par hostname
mu sync.RWMutex
}
// NewRegistry crée un nouveau registre de configurations.
func NewRegistry(configDir string) *Registry {
return &Registry{
configDir: configDir,
apps: make(map[string]*AppConfig),
byHost: make(map[string]*AppConfig),
}
}
// Load charge toutes les configurations depuis le répertoire routes.
func (r *Registry) Load() error {
r.mu.Lock()
defer r.mu.Unlock()
routesDir := filepath.Join(r.configDir, "routes")
entries, err := os.ReadDir(routesDir)
if err != nil {
return fmt.Errorf("read routes dir: %w", err)
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
continue
}
path := filepath.Join(routesDir, entry.Name())
cfg, err := r.loadAppConfig(path)
if err != nil {
return fmt.Errorf("load %s: %w", entry.Name(), err)
}
r.apps[cfg.App] = cfg
for _, host := range cfg.Hosts {
r.byHost[host] = cfg
}
}
return nil
}
// loadAppConfig charge une configuration d'application depuis un fichier YAML.
func (r *Registry) loadAppConfig(path string) (*AppConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg AppConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
// Charger le mot de passe DB depuis le fichier
if cfg.Database.PasswordFile != "" {
passData, err := os.ReadFile(cfg.Database.PasswordFile)
if err != nil {
return nil, fmt.Errorf("read db password file: %w", err)
}
cfg.Database.password = strings.TrimSpace(string(passData))
}
// Charger le secret JWT depuis le fichier
if cfg.Auth.JWTSecretFile != "" {
secretData, err := os.ReadFile(cfg.Auth.JWTSecretFile)
if err != nil {
return nil, fmt.Errorf("read jwt secret file: %w", err)
}
cfg.Auth.jwtSecret = strings.TrimSpace(string(secretData))
}
// Port DB par défaut
if cfg.Database.Port == 0 {
cfg.Database.Port = 3306
}
// Expiry JWT par défaut
if cfg.Auth.JWTExpiry == "" {
cfg.Auth.JWTExpiry = "24h"
}
// Charger les requêtes depuis config/queries/{app}/
cfg.Queries = r.loadQueries(cfg.App)
return &cfg, nil
}
// loadQueries charge les fichiers de requêtes pour une application.
func (r *Registry) loadQueries(appID string) *Queries {
queriesDir := filepath.Join(r.configDir, "queries", appID)
entries, err := os.ReadDir(queriesDir)
if err != nil {
return nil // Pas de répertoire queries, c'est OK
}
q := &Queries{
files: make(map[string]map[string]any),
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
continue
}
domain := strings.TrimSuffix(entry.Name(), ".yaml")
path := filepath.Join(queriesDir, entry.Name())
data, err := os.ReadFile(path)
if err != nil {
continue
}
var content map[string]any
if err := yaml.Unmarshal(data, &content); err != nil {
continue
}
q.files[domain] = content
}
return q
}
// GetByApp retourne la configuration d'une application par son ID.
func (r *Registry) GetByApp(appID string) (*AppConfig, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
cfg, ok := r.apps[appID]
return cfg, ok
}
// GetByHost retourne la configuration d'une application par son hostname.
func (r *Registry) GetByHost(host string) (*AppConfig, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
cfg, ok := r.byHost[host]
return cfg, ok
}
// Apps retourne la liste des IDs d'applications chargées.
func (r *Registry) Apps() []string {
r.mu.RLock()
defer r.mu.RUnlock()
apps := make([]string, 0, len(r.apps))
for app := range r.apps {
apps = append(apps, app)
}
return apps
}