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:
19
DOCTECH.md
19
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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
8
TODO.md
8
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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}}
|
||||
|
||||
Reference in New Issue
Block a user