SOGOMS v1.0.5 - Auto-génération login_data et version UI

- Génération automatique de login_data dans auth.yaml après scan DB
- Tables avec filter:owner incluses dans login_data pour login enrichi
- Affichage version SOGOMS dans l'interface admin (login + header)
- Documentation mise à jour (DOCTECH.md, README.md, TODO.md)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-22 15:41:49 +01:00
parent 65da4efdad
commit 1274400b08
9 changed files with 178 additions and 6 deletions

View File

@@ -2,8 +2,8 @@
**Service Oriented GO MicroServices** - Plateforme SaaS modulaire multi-tenant. **Service Oriented GO MicroServices** - Plateforme SaaS modulaire multi-tenant.
Version: 1.0.1 Version: 1.0.5
Date: 16 décembre 2025 Date: 22 décembre 2025
--- ---
@@ -226,9 +226,24 @@ Interface d'administration web pour gérer les applications SOGOMS.
- `POST /admin/login` : authentification - `POST /admin/login` : authentification
- `GET /admin/` : dashboard principal - `GET /admin/` : dashboard principal
- `POST /admin/logout` : déconnexion - `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/apps` : liste apps (htmx partial)
- `GET /admin/api/services/health` : statut services (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 :** **Configuration :**
```yaml ```yaml
# /secrets/admin_users.yaml # /secrets/admin_users.yaml

View File

@@ -18,7 +18,7 @@ schema.yaml → SOGOMS → API REST + Auth + CRUD + Push
- **Sécurisé** : JWT, isolation par user_id, bcrypt - **Sécurisé** : JWT, isolation par user_id, bcrypt
- **Auto-supervisé** : health checks, restart automatique - **Auto-supervisé** : health checks, restart automatique
- **Temps réel** : push MQTT vers les applications (roadmap) - **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 ## Services actuels
@@ -30,7 +30,7 @@ schema.yaml → SOGOMS → API REST + Auth + CRUD + Push
| `sogoms-logs` | Logging centralisé | Stable | | `sogoms-logs` | Logging centralisé | Stable |
| `sogoms-smtp` | Envoi emails, templates | Stable | | `sogoms-smtp` | Envoi emails, templates | Stable |
| `sogoms-cron` | Tâches planifiées | Stable | | `sogoms-cron` | Tâches planifiées | Stable |
| `sogoms-admin` | Interface web administration | Stable | | `sogoms-admin` | Interface web administration, scan DB | Stable |
## Roadmap ## Roadmap

View File

@@ -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 : liste routes générées
- [ ] Page détail app : dictionnaire des données (types, contraintes) - [ ] Page détail app : dictionnaire des données (types, contraintes)
- [ ] Indicateur : schema synchronisé / désynchronisé avec DB - [ ] 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 ### 19e. Gestion des secrets

View File

@@ -1 +1 @@
1.0.3 1.0.5

View File

@@ -18,6 +18,7 @@ type AdminServer struct {
adminCfg *admin.AdminConfig adminCfg *admin.AdminConfig
registry *config.Registry registry *config.Registry
sessions *SessionStore sessions *SessionStore
version string
rateLimiter *RateLimiter rateLimiter *RateLimiter
perms *admin.PermissionChecker perms *admin.PermissionChecker
audit *admin.AuditLogger audit *admin.AuditLogger
@@ -585,6 +586,14 @@ func (s *AdminServer) HandleAppScanDB(w http.ResponseWriter, r *http.Request) {
return 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 // Recharger le registry local
if err := s.registry.Load(); err != nil { if err := s.registry.Load(); err != nil {
log.Printf("[admin] reload registry error: %v", err) 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) { func (s *AdminServer) render(w http.ResponseWriter, name string, data map[string]any) {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
// Ajouter la version à toutes les pages
data["SogVersion"] = s.version
tmpl := s.getTemplates() tmpl := s.getTemplates()
if err := tmpl.ExecuteTemplate(w, name, data); err != nil { if err := tmpl.ExecuteTemplate(w, name, data); err != nil {
log.Printf("[admin] template error: %v", err) log.Printf("[admin] template error: %v", err)

View File

@@ -17,6 +17,7 @@ import (
"sogoms.com/internal/admin" "sogoms.com/internal/admin"
"sogoms.com/internal/config" "sogoms.com/internal/config"
"sogoms.com/internal/protocol" "sogoms.com/internal/protocol"
"sogoms.com/internal/version"
) )
//go:embed templates/*.html templates/partials/*.html //go:embed templates/*.html templates/partials/*.html
@@ -99,6 +100,7 @@ func main() {
adminCfg: adminCfg, adminCfg: adminCfg,
registry: registry, registry: registry,
sessions: sessions, sessions: sessions,
version: version.Version,
rateLimiter: rateLimiter, rateLimiter: rateLimiter,
perms: perms, perms: perms,
audit: audit, audit: audit,

View File

@@ -417,6 +417,138 @@ func hasUserID(tableData map[string]any) bool {
return false 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. // ReloadGateway demande à sogoctl de recharger sogoway.
func (sp *ServicePool) ReloadGateway() error { func (sp *ServicePool) ReloadGateway() error {
conn, err := net.DialTimeout("unix", "/run/sogoctl.sock", 2*time.Second) conn, err := net.DialTimeout("unix", "/run/sogoctl.sock", 2*time.Second)

View File

@@ -63,6 +63,9 @@
<button type="submit">Se connecter</button> <button type="submit">Se connecter</button>
</form> </form>
<footer style="text-align: center; margin-top: 1rem; font-size: 0.8rem; color: var(--pico-muted-color);">
v{{.SogVersion}}
</footer>
</article> </article>
</body> </body>
</html> </html>

View File

@@ -25,7 +25,7 @@
<header class="container"> <header class="container">
<nav> <nav>
<ul> <ul>
<li><a href="/admin/" class="logo"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#1095c1" d="M0,3v8H11V0H3A3,3,0,0,0,0,3Z"/><path fill="#1095c1" d="M21,0H13V11H24V3A3,3,0,0,0,21,0Z"/><path fill="#1095c1" d="M0,21a3,3,0,0,0,3,3h8V13H0Z"/><path fill="#1095c1" d="M13,24h8a3,3,0,0,0,3-3V13H13Z"/></svg>SOGO<span>MS</span></a></li> <li><a href="/admin/" class="logo"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#1095c1" d="M0,3v8H11V0H3A3,3,0,0,0,0,3Z"/><path fill="#1095c1" d="M21,0H13V11H24V3A3,3,0,0,0,21,0Z"/><path fill="#1095c1" d="M0,21a3,3,0,0,0,3,3h8V13H0Z"/><path fill="#1095c1" d="M13,24h8a3,3,0,0,0,3-3V13H13Z"/></svg>SOGO<span>MS</span> <small style="font-weight:normal;color:var(--pico-muted-color)">v{{.SogVersion}}</small></a></li>
</ul> </ul>
<ul> <ul>
{{if .User}} {{if .User}}