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 @@
+