diff --git a/DOCTECH.md b/DOCTECH.md index 7be9467..053bbaf 100644 --- a/DOCTECH.md +++ b/DOCTECH.md @@ -2,8 +2,8 @@ **Service Oriented GO MicroServices** - Plateforme SaaS modulaire multi-tenant. -Version: 1.0.1 -Date: 16 décembre 2025 +Version: 1.0.5 +Date: 22 décembre 2025 --- @@ -226,9 +226,24 @@ Interface d'administration web pour gérer les applications SOGOMS. - `POST /admin/login` : authentification - `GET /admin/` : dashboard principal - `POST /admin/logout` : déconnexion +- `GET /admin/apps/{app}` : détail application +- `POST /admin/apps/{app}/scan` : scan DB et génération schema - `GET /admin/api/apps` : liste apps (htmx partial) - `GET /admin/api/services/health` : statut services (htmx partial) +**Scan DB et génération automatique :** + +Le bouton "Scanner la base" sur la page détail d'une app : +1. Introspection de la DB via `INFORMATION_SCHEMA` +2. Génération de `schema.yaml` (tables, colonnes, types, contraintes) +3. Génération automatique de `login_data` dans `queries/auth.yaml` +4. Rechargement du registry et de sogoway (SIGHUP) + +Les tables avec colonne `user_id` reçoivent automatiquement : +- `filter: owner` pour filtrage par utilisateur +- CRUD activé (list, show, create, update, delete) +- Requête SELECT dans `login_data` pour le login enrichi + **Configuration :** ```yaml # /secrets/admin_users.yaml diff --git a/README.md b/README.md index 93a94fa..4fee38a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ schema.yaml → SOGOMS → API REST + Auth + CRUD + Push - **Sécurisé** : JWT, isolation par user_id, bcrypt - **Auto-supervisé** : health checks, restart automatique - **Temps réel** : push MQTT vers les applications (roadmap) -- **Schema-driven** : génération d'API depuis la structure DB (roadmap) +- **Schema-driven** : génération d'API et queries depuis la structure DB ## Services actuels @@ -30,7 +30,7 @@ schema.yaml → SOGOMS → API REST + Auth + CRUD + Push | `sogoms-logs` | Logging centralisé | Stable | | `sogoms-smtp` | Envoi emails, templates | Stable | | `sogoms-cron` | Tâches planifiées | Stable | -| `sogoms-admin` | Interface web administration | Stable | +| `sogoms-admin` | Interface web administration, scan DB | Stable | ## Roadmap diff --git a/TODO.md b/TODO.md index 1e43f9e..afbfc0c 100755 --- a/TODO.md +++ b/TODO.md @@ -408,6 +408,14 @@ Note : le bouton "Scanner la base" (19b) fait office d'Update Schema. - [ ] Page détail app : liste routes générées - [ ] Page détail app : dictionnaire des données (types, contraintes) - [ ] Indicateur : schema synchronisé / désynchronisé avec DB +- [ ] Modale détail table : colonnes (nom, type, nullable, default, contraintes) +- [ ] Modale détail route : query SQL, filtres, champs autorisés + +### 19f. Génération auto login_data + +- [x] Après scan schema, regénérer `login_data` dans `queries/auth.yaml` +- [x] Pour chaque table avec `filter: owner` : SELECT toutes colonnes WHERE user_id = ? +- [x] Préserver le reste du fichier auth.yaml (user_by_email, etc.) ### 19e. Gestion des secrets diff --git a/VERSION b/VERSION index 21e8796..1464c52 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.3 +1.0.5 \ No newline at end of file diff --git a/cmd/sogoms/admin/handlers.go b/cmd/sogoms/admin/handlers.go index 1773f1b..1237785 100644 --- a/cmd/sogoms/admin/handlers.go +++ b/cmd/sogoms/admin/handlers.go @@ -18,6 +18,7 @@ type AdminServer struct { adminCfg *admin.AdminConfig registry *config.Registry sessions *SessionStore + version string rateLimiter *RateLimiter perms *admin.PermissionChecker audit *admin.AuditLogger @@ -585,6 +586,14 @@ func (s *AdminServer) HandleAppScanDB(w http.ResponseWriter, r *http.Request) { return } + // Mettre à jour login_data dans auth.yaml + if err := UpdateLoginData(appID); err != nil { + log.Printf("[admin] update login_data error: %v", err) + // On ne bloque pas, le scan a réussi + } else { + log.Printf("[admin] login_data updated for app: %s", appID) + } + // Recharger le registry local if err := s.registry.Load(); err != nil { log.Printf("[admin] reload registry error: %v", err) @@ -614,6 +623,9 @@ func (s *AdminServer) HandleAppScanDB(w http.ResponseWriter, r *http.Request) { func (s *AdminServer) render(w http.ResponseWriter, name string, data map[string]any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") + // Ajouter la version à toutes les pages + data["SogVersion"] = s.version + tmpl := s.getTemplates() if err := tmpl.ExecuteTemplate(w, name, data); err != nil { log.Printf("[admin] template error: %v", err) diff --git a/cmd/sogoms/admin/main.go b/cmd/sogoms/admin/main.go index 9064af9..eeb7964 100644 --- a/cmd/sogoms/admin/main.go +++ b/cmd/sogoms/admin/main.go @@ -17,6 +17,7 @@ import ( "sogoms.com/internal/admin" "sogoms.com/internal/config" "sogoms.com/internal/protocol" + "sogoms.com/internal/version" ) //go:embed templates/*.html templates/partials/*.html @@ -99,6 +100,7 @@ func main() { adminCfg: adminCfg, registry: registry, sessions: sessions, + version: version.Version, rateLimiter: rateLimiter, perms: perms, audit: audit, diff --git a/cmd/sogoms/admin/services.go b/cmd/sogoms/admin/services.go index a05e6c5..83641c6 100644 --- a/cmd/sogoms/admin/services.go +++ b/cmd/sogoms/admin/services.go @@ -417,6 +417,138 @@ func hasUserID(tableData map[string]any) bool { 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 +} + // ReloadGateway demande à sogoctl de recharger sogoway. func (sp *ServicePool) ReloadGateway() error { conn, err := net.DialTimeout("unix", "/run/sogoctl.sock", 2*time.Second) diff --git a/cmd/sogoms/admin/templates/login.html b/cmd/sogoms/admin/templates/login.html index 7cd9bf7..8491f2a 100644 --- a/cmd/sogoms/admin/templates/login.html +++ b/cmd/sogoms/admin/templates/login.html @@ -63,6 +63,9 @@ + diff --git a/cmd/sogoms/admin/templates/partials/header.html b/cmd/sogoms/admin/templates/partials/header.html index 3ff302c..336b556 100644 --- a/cmd/sogoms/admin/templates/partials/header.html +++ b/cmd/sogoms/admin/templates/partials/header.html @@ -25,7 +25,7 @@