veza/veza-backend-api/internal/services/chat_service.go
senke a3c055efbb [BE-API-006] be-api: Implement chat stats endpoint
- Added GetStats method to ChatService with database access
- Returns active_users (distinct users who sent messages in last 24h)
- Returns total_messages (non-deleted messages count)
- Returns rooms_active (rooms with messages in last 24h)
- Added GetStats handler and GET /chat/stats route
- Updated ChatService to use NewChatServiceWithDB for database access

Phase: PHASE-2
Priority: P1
Progress: 15/267 (5.6%)
2025-12-23 01:51:49 +01:00

126 lines
3.3 KiB
Go

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
}