Files
sogoms/cmd/sogoms/admin/session.go
Pierre 65da4efdad SOGOMS v1.0.3 - Admin UI, Cron, Config reload
Phase 13 : sogoms-cron
- Jobs planifiés avec schedule cron standard
- Types: query_email, http, service
- Actions: list, trigger, status

Phase 16 : Réorganisation config/apps/{app}/
- Tous les fichiers d'une app dans un seul dossier
- Migration prokov vers nouvelle structure

Phase 17 : sogoms-admin
- Interface web d'administration (Go templates + htmx)
- Auth sessions cookies signées HMAC-SHA256
- Rôles super_admin / app_admin avec permissions

Phase 19 : Création d'app via Admin UI
- Formulaire création app avec config DB/auth
- Bouton "Scanner la base" : introspection + schema.yaml
- Rechargement automatique sogoway via SIGHUP

Infrastructure :
- sogoctl : socket de contrôle /run/sogoctl.sock
- sogoway : reload config sur SIGHUP sans restart

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 20:30:56 +01:00

215 lines
4.8 KiB
Go

package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"net/http"
"strings"
"sync"
"time"
"sogoms.com/internal/admin"
)
// Session représente une session utilisateur.
type Session struct {
ID string
Username string
Role string
CSRFToken string
CreatedAt time.Time
ExpiresAt time.Time
IP string
UserAgent string
}
// IsExpired vérifie si la session a expiré.
func (s *Session) IsExpired() bool {
return time.Now().After(s.ExpiresAt)
}
// SessionStore gère les sessions en mémoire.
type SessionStore struct {
sessions map[string]*Session
config *admin.SessionConfig
mu sync.RWMutex
}
// NewSessionStore crée un nouveau store de sessions.
func NewSessionStore(config *admin.SessionConfig) *SessionStore {
return &SessionStore{
sessions: make(map[string]*Session),
config: config,
}
}
// Create crée une nouvelle session.
func (s *SessionStore) Create(username, role, ip, userAgent string) (*Session, error) {
sessionID, err := generateSecureToken(32)
if err != nil {
return nil, err
}
csrfToken, err := generateSecureToken(32)
if err != nil {
return nil, err
}
now := time.Now()
session := &Session{
ID: sessionID,
Username: username,
Role: role,
CSRFToken: csrfToken,
CreatedAt: now,
ExpiresAt: now.Add(time.Duration(s.config.MaxAge) * time.Second),
IP: ip,
UserAgent: userAgent,
}
s.mu.Lock()
s.sessions[sessionID] = session
s.mu.Unlock()
return session, nil
}
// Get récupère une session par son ID.
func (s *SessionStore) Get(sessionID string) (*Session, bool) {
s.mu.RLock()
session, ok := s.sessions[sessionID]
s.mu.RUnlock()
if !ok || session.IsExpired() {
return nil, false
}
return session, true
}
// Delete supprime une session.
func (s *SessionStore) Delete(sessionID string) {
s.mu.Lock()
delete(s.sessions, sessionID)
s.mu.Unlock()
}
// Refresh prolonge la durée de vie d'une session (sliding expiration).
func (s *SessionStore) Refresh(sessionID string) {
s.mu.Lock()
if session, ok := s.sessions[sessionID]; ok {
session.ExpiresAt = time.Now().Add(time.Duration(s.config.MaxAge) * time.Second)
}
s.mu.Unlock()
}
// Cleanup supprime les sessions expirées.
func (s *SessionStore) Cleanup() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
s.mu.Lock()
now := time.Now()
for id, session := range s.sessions {
if now.After(session.ExpiresAt) {
delete(s.sessions, id)
}
}
s.mu.Unlock()
}
}
// Count retourne le nombre de sessions actives.
func (s *SessionStore) Count() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.sessions)
}
// SetCookie définit le cookie de session dans la réponse.
func (s *SessionStore) SetCookie(w http.ResponseWriter, session *Session) {
// Signer le session ID
signature := s.sign(session.ID)
value := session.ID + "." + signature
http.SetCookie(w, &http.Cookie{
Name: s.config.CookieName,
Value: value,
Path: "/admin",
MaxAge: s.config.MaxAge,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
}
// ClearCookie supprime le cookie de session.
func (s *SessionStore) ClearCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: s.config.CookieName,
Value: "",
Path: "/admin",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
}
// GetSessionFromRequest extrait et valide la session depuis le cookie.
func (s *SessionStore) GetSessionFromRequest(r *http.Request) (*Session, error) {
cookie, err := r.Cookie(s.config.CookieName)
if err != nil {
return nil, fmt.Errorf("no session cookie")
}
// Séparer ID et signature
parts := strings.SplitN(cookie.Value, ".", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid cookie format")
}
sessionID := parts[0]
signature := parts[1]
// Vérifier la signature
if !s.verify(sessionID, signature) {
return nil, fmt.Errorf("invalid cookie signature")
}
// Récupérer la session
session, ok := s.Get(sessionID)
if !ok {
return nil, fmt.Errorf("session not found or expired")
}
return session, nil
}
// sign signe une donnée avec HMAC-SHA256.
func (s *SessionStore) sign(data string) string {
h := hmac.New(sha256.New, []byte(s.config.Secret))
h.Write([]byte(data))
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
}
// verify vérifie une signature HMAC.
func (s *SessionStore) verify(data, signature string) bool {
expected := s.sign(data)
return hmac.Equal([]byte(expected), []byte(signature))
}
// generateSecureToken génère un token aléatoire sécurisé.
func generateSecureToken(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}