package services import ( "context" "errors" "fmt" "github.com/google/uuid" "time" "github.com/golang-jwt/jwt/v5" "go.uber.org/zap" "gorm.io/gorm" "veza-backend-api/internal/models" ) type ChatService struct { jwtSecret string logger *zap.Logger db *gorm.DB } func NewChatService(jwtSecret string, logger *zap.Logger) *ChatService { if logger == nil { logger = zap.NewNop() } return &ChatService{ jwtSecret: jwtSecret, logger: logger, } } // NewChatServiceWithDB crée un nouveau ChatService avec accès à la base de données func NewChatServiceWithDB(jwtSecret string, db *gorm.DB, logger *zap.Logger) *ChatService { if logger == nil { logger = zap.NewNop() } return &ChatService{ jwtSecret: jwtSecret, logger: logger, db: db, } } type ChatTokenResponse struct { Token string `json:"token"` ExpiresIn int64 `json:"expires_in"` WSUrl string `json:"ws_url"` } func (s *ChatService) GenerateToken(userID uuid.UUID, username string) (*ChatTokenResponse, error) { if s.jwtSecret == "" { return nil, errors.New("JWT secret is not configured") } now := time.Now() expiration := 15 * time.Minute exp := now.Add(expiration) claims := jwt.MapClaims{ "sub": userID.String(), "name": username, "aud": "veza-chat", "iss": "veza-backend", "iat": now.Unix(), "exp": exp.Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString([]byte(s.jwtSecret)) if err != nil { return nil, fmt.Errorf("failed to sign token: %w", err) } return &ChatTokenResponse{ Token: tokenString, ExpiresIn: int64(expiration.Seconds()), WSUrl: "/ws", // Relative path, frontend appends base URL }, nil } // ChatStats représente les statistiques du chat type ChatStats struct { ActiveUsers int64 `json:"active_users"` TotalMessages int64 `json:"total_messages"` RoomsActive int64 `json:"rooms_active"` } // GetStats récupère les statistiques du chat // BE-API-006: Implement chat stats endpoint func (s *ChatService) GetStats(ctx context.Context) (*ChatStats, error) { if s.db == nil { return nil, errors.New("database connection not available") } stats := &ChatStats{} // Total messages (non supprimés) if err := s.db.WithContext(ctx).Model(&models.Message{}). Where("is_deleted = ?", false). Count(&stats.TotalMessages).Error; err != nil { return nil, fmt.Errorf("failed to count total messages: %w", err) } // Active users: utilisateurs distincts qui ont envoyé des messages dans les dernières 24h activeSince := time.Now().Add(-24 * time.Hour) var activeUsersCount int64 if err := s.db.WithContext(ctx).Model(&models.Message{}). Where("is_deleted = ? AND created_at >= ?", false, activeSince). Distinct("user_id"). Count(&activeUsersCount).Error; err != nil { return nil, fmt.Errorf("failed to count active users: %w", err) } stats.ActiveUsers = activeUsersCount // Rooms actives: rooms qui ont eu des messages dans les dernières 24h var roomsActiveCount int64 if err := s.db.WithContext(ctx).Model(&models.Message{}). Where("is_deleted = ? AND created_at >= ?", false, activeSince). Distinct("room_id"). Count(&roomsActiveCount).Error; err != nil { return nil, fmt.Errorf("failed to count active rooms: %w", err) } stats.RoomsActive = roomsActiveCount return stats, nil }