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

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

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@
<header class="container">
<nav>
<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>
{{if .User}}