From 68ed71aa62c0b0d9b96e9b09ac059e71ff976adb Mon Sep 17 00:00:00 2001 From: Mohamed Ramadan Date: Fri, 6 Mar 2026 22:36:24 +0200 Subject: [PATCH 1/5] refactor: remove summarizer_repository and extract session, chunk, and transcript repos Replace the monolithic SummarizerRepository with three focused repositories: SummarizerSessionRepository, AudioChunkRepository, and TranscriptRepository. Update service constructors and logic (summarizer, transcription, normalization, summarization, user, meeting) to use the new repos and adjusted method names, update workers and main wiring accordingly, and remove the old summarizer repository file. This refactor separates concerns for session, chunk, and transcript operations and updates cleanup/status flows to use the new repository interfaces. --- backend/cmd/server/main.go | 20 +-- .../repositories/audio_chunk_repository.go | 45 ++++++ .../repositories/summarizer_repository.go | 144 ------------------ .../summarizer_session_repository.go | 96 ++++++++++++ .../repositories/transcript_repository.go | 29 ++++ backend/internal/services/meeting_service.go | 4 + .../services/normalization_service.go | 18 ++- .../services/summarization_service.go | 22 +-- .../internal/services/summarizer_service.go | 55 +++---- .../services/transcription_service.go | 26 ++-- backend/internal/services/user_service.go | 12 +- .../internal/workers/normalization_worker.go | 6 +- .../internal/workers/summarization_worker.go | 6 +- .../internal/workers/transcription_worker.go | 6 +- 14 files changed, 265 insertions(+), 224 deletions(-) create mode 100644 backend/internal/repositories/audio_chunk_repository.go delete mode 100644 backend/internal/repositories/summarizer_repository.go create mode 100644 backend/internal/repositories/summarizer_session_repository.go create mode 100644 backend/internal/repositories/transcript_repository.go diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 909e2bf..5b2717b 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -64,20 +64,22 @@ func main() { // Initialize repositories userRepo := repositories.NewUserRepository(database.GetDB()) meetingRepo := repositories.NewMeetingRepository(database.GetDB()) - summarizerRepo := repositories.NewSummarizerRepository(database.GetDB()) + sessionRepo := repositories.NewSummarizerSessionRepository(database.GetDB()) + chunkRepo := repositories.NewAudioChunkRepository(database.GetDB()) + transcriptRepo := repositories.NewTranscriptRepository(database.GetDB()) // Initialize services - userService := services.NewUserService(userRepo, meetingRepo) meetingService := services.NewMeetingService(meetingRepo) + userService := services.NewUserService(userRepo, meetingService) livekitService := services.NewLiveKitService(cfg) openRouterService := services.NewOpenRouterService(cfg) emailService := services.NewEmailService(cfg) // Dependency chain: SummarizationService <- NormalizationService <- TranscriptionService <- SummarizerService - summarizationService := services.NewSummarizationService(summarizerRepo, userRepo, openRouterService, emailService, cfg) - normalizationService := services.NewNormalizationService(summarizerRepo, summarizationService) - transcriptionService := services.NewTranscriptionService(summarizerRepo, normalizationService, cfg) - summarizerService := services.NewSummarizerService(summarizerRepo, meetingRepo, userRepo, livekitService, transcriptionService, cfg) + summarizationService := services.NewSummarizationService(sessionRepo, userService, openRouterService, emailService, cfg) + normalizationService := services.NewNormalizationService(sessionRepo, transcriptRepo, summarizationService) + transcriptionService := services.NewTranscriptionService(sessionRepo, chunkRepo, transcriptRepo, normalizationService, cfg) + summarizerService := services.NewSummarizerService(sessionRepo, chunkRepo, transcriptRepo, meetingService, livekitService, transcriptionService, cfg) // Initialize handlers userHandler := handlers.NewUserHandler(userService) @@ -90,13 +92,13 @@ func main() { // Initialize workers // Transcription worker: Run every 60 minutes, process sessions stuck for > 15 minutes - transcriptionWorker := workers.NewTranscriptionWorker(summarizerRepo, transcriptionService, 60*time.Minute, 15*time.Minute) + transcriptionWorker := workers.NewTranscriptionWorker(sessionRepo, transcriptionService, 60*time.Minute, 15*time.Minute) go transcriptionWorker.Start() // Normalization worker: Run every 60 minutes, process sessions stuck for > 15 minutes - normalizationWorker := workers.NewNormalizationWorker(summarizerRepo, normalizationService, 60*time.Minute, 15*time.Minute) + normalizationWorker := workers.NewNormalizationWorker(sessionRepo, normalizationService, 60*time.Minute, 15*time.Minute) go normalizationWorker.Start() // Summarization worker: Run every 60 minutes, process sessions stuck for > 15 minutes - summarizationWorker := workers.NewSummarizationWorker(summarizerRepo, summarizationService, 60*time.Minute, 15*time.Minute) + summarizationWorker := workers.NewSummarizationWorker(sessionRepo, summarizationService, 60*time.Minute, 15*time.Minute) go summarizationWorker.Start() // Setup routes diff --git a/backend/internal/repositories/audio_chunk_repository.go b/backend/internal/repositories/audio_chunk_repository.go new file mode 100644 index 0000000..5be9251 --- /dev/null +++ b/backend/internal/repositories/audio_chunk_repository.go @@ -0,0 +1,45 @@ +package repositories + +import ( + "mini-meeting/internal/models" + + "gorm.io/gorm" +) + +type AudioChunkRepository struct { + db *gorm.DB +} + +func NewAudioChunkRepository(db *gorm.DB) *AudioChunkRepository { + return &AudioChunkRepository{db: db} +} + +func (r *AudioChunkRepository) Create(chunk *models.AudioChunk) error { + return r.db.Create(chunk).Error +} + +func (r *AudioChunkRepository) FindBySessionID(sessionID uint) ([]models.AudioChunk, error) { + var chunks []models.AudioChunk + err := r.db.Where("session_id = ?", sessionID). + Order("user_identity ASC, chunk_index ASC"). + Find(&chunks).Error + return chunks, err +} + +func (r *AudioChunkRepository) FindBySessionAndUser(sessionID uint, userIdentity string) ([]models.AudioChunk, error) { + var chunks []models.AudioChunk + err := r.db.Where("session_id = ? AND user_identity = ?", sessionID, userIdentity). + Order("chunk_index ASC"). + Find(&chunks).Error + return chunks, err +} + +func (r *AudioChunkRepository) CountBySessionID(sessionID uint) (int64, error) { + var count int64 + err := r.db.Model(&models.AudioChunk{}).Where("session_id = ?", sessionID).Count(&count).Error + return count, err +} + +func (r *AudioChunkRepository) DeleteBySessionID(sessionID uint) error { + return r.db.Where("session_id = ?", sessionID).Delete(&models.AudioChunk{}).Error +} diff --git a/backend/internal/repositories/summarizer_repository.go b/backend/internal/repositories/summarizer_repository.go deleted file mode 100644 index 0443409..0000000 --- a/backend/internal/repositories/summarizer_repository.go +++ /dev/null @@ -1,144 +0,0 @@ -package repositories - -import ( - "mini-meeting/internal/models" - "time" - - "gorm.io/gorm" -) - -type SummarizerRepository struct { - db *gorm.DB -} - -func NewSummarizerRepository(db *gorm.DB) *SummarizerRepository { - return &SummarizerRepository{db: db} -} - -// Session operations - -func (r *SummarizerRepository) CreateSession(session *models.SummarizerSession) error { - return r.db.Create(session).Error -} - -func (r *SummarizerRepository) FindSessionByID(id uint) (*models.SummarizerSession, error) { - var session models.SummarizerSession - err := r.db.Preload("Meeting").First(&session, id).Error - if err != nil { - return nil, err - } - return &session, nil -} - -func (r *SummarizerRepository) FindActiveSessionByMeetingID(meetingID uint) (*models.SummarizerSession, error) { - var session models.SummarizerSession - err := r.db.Where("meeting_id = ? AND status = ?", meetingID, models.StatusStarted). - First(&session).Error - if err != nil { - return nil, err - } - return &session, nil -} - -func (r *SummarizerRepository) UpdateSessionStatus(id uint, status models.SummarizerSessionStatus, error *string, endedAt *time.Time) error { - updates := map[string]interface{}{ - "status": status, - "updated_at": time.Now(), - } - if error != nil { - updates["error"] = error - } - if endedAt != nil { - updates["ended_at"] = endedAt - } - return r.db.Model(&models.SummarizerSession{}).Where("id = ?", id).Updates(updates).Error -} - -func (r *SummarizerRepository) FindSessionsByMeetingID(meetingID uint) ([]models.SummarizerSession, error) { - var sessions []models.SummarizerSession - err := r.db.Where("meeting_id = ?", meetingID).Order("created_at DESC").Find(&sessions).Error - return sessions, err -} - -func (r *SummarizerRepository) FindStuckSessions(status models.SummarizerSessionStatus, cutoffTime time.Time) ([]models.SummarizerSession, error) { - var sessions []models.SummarizerSession - err := r.db.Where("status = ? AND updated_at < ?", status, cutoffTime).Find(&sessions).Error - return sessions, err -} - -func (r *SummarizerRepository) FindAllByUserIDPaginated(userID uint, page, pageSize int) ([]models.SummarizerSession, int64, error) { - var sessions []models.SummarizerSession - var total int64 - - query := r.db.Model(&models.SummarizerSession{}).Where("user_id = ?", userID) - - if err := query.Count(&total).Error; err != nil { - return nil, 0, err - } - - offset := (page - 1) * pageSize - err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&sessions).Error - if err != nil { - return nil, 0, err - } - - return sessions, total, nil -} - -func (r *SummarizerRepository) DeleteSession(id uint) error { - return r.db.Delete(&models.SummarizerSession{}, id).Error -} - -func (r *SummarizerRepository) CreateAudioChunk(chunk *models.AudioChunk) error { - return r.db.Create(chunk).Error -} - -func (r *SummarizerRepository) FindChunksBySessionID(sessionID uint) ([]models.AudioChunk, error) { - var chunks []models.AudioChunk - err := r.db.Where("session_id = ?", sessionID). - Order("user_identity ASC, chunk_index ASC"). - Find(&chunks).Error - return chunks, err -} - -func (r *SummarizerRepository) FindChunksBySessionAndUser(sessionID uint, userIdentity string) ([]models.AudioChunk, error) { - var chunks []models.AudioChunk - err := r.db.Where("session_id = ? AND user_identity = ?", sessionID, userIdentity). - Order("chunk_index ASC"). - Find(&chunks).Error - return chunks, err -} - -func (r *SummarizerRepository) CountChunksBySessionID(sessionID uint) (int64, error) { - var count int64 - err := r.db.Model(&models.AudioChunk{}).Where("session_id = ?", sessionID).Count(&count).Error - return count, err -} - -func (r *SummarizerRepository) DeleteChunksBySessionID(sessionID uint) error { - return r.db.Where("session_id = ?", sessionID).Delete(&models.AudioChunk{}).Error -} - -// Transcript operations - -func (r *SummarizerRepository) CreateTranscript(transcript *models.Transcript) error { - return r.db.Create(transcript).Error -} - -func (r *SummarizerRepository) FindTranscriptsBySessionID(sessionID uint) ([]models.Transcript, error) { - var transcripts []models.Transcript - err := r.db.Where("session_id = ?", sessionID).Order("start_time ASC").Find(&transcripts).Error - return transcripts, err -} - -func (r *SummarizerRepository) DeleteTranscriptsBySessionID(sessionID uint) error { - return r.db.Where("session_id = ?", sessionID).Delete(&models.Transcript{}).Error -} - -func (r *SummarizerRepository) UpdateSessionTranscript(id uint, transcript string) error { - return r.db.Model(&models.SummarizerSession{}).Where("id = ?", id).Update("transcript", transcript).Error -} - -func (r *SummarizerRepository) UpdateSessionSummary(id uint, summary string) error { - return r.db.Model(&models.SummarizerSession{}).Where("id = ?", id).Update("summary", summary).Error -} diff --git a/backend/internal/repositories/summarizer_session_repository.go b/backend/internal/repositories/summarizer_session_repository.go new file mode 100644 index 0000000..55584a9 --- /dev/null +++ b/backend/internal/repositories/summarizer_session_repository.go @@ -0,0 +1,96 @@ +package repositories + +import ( + "mini-meeting/internal/models" + "time" + + "gorm.io/gorm" +) + +type SummarizerSessionRepository struct { + db *gorm.DB +} + +func NewSummarizerSessionRepository(db *gorm.DB) *SummarizerSessionRepository { + return &SummarizerSessionRepository{db: db} +} + +func (r *SummarizerSessionRepository) Create(session *models.SummarizerSession) error { + return r.db.Create(session).Error +} + +func (r *SummarizerSessionRepository) FindByID(id uint) (*models.SummarizerSession, error) { + var session models.SummarizerSession + err := r.db.Preload("Meeting").First(&session, id).Error + if err != nil { + return nil, err + } + return &session, nil +} + +func (r *SummarizerSessionRepository) FindActiveByMeetingID(meetingID uint) (*models.SummarizerSession, error) { + var session models.SummarizerSession + err := r.db.Where("meeting_id = ? AND status = ?", meetingID, models.StatusStarted). + First(&session).Error + if err != nil { + return nil, err + } + return &session, nil +} + +func (r *SummarizerSessionRepository) UpdateStatus(id uint, status models.SummarizerSessionStatus, sessionError *string, endedAt *time.Time) error { + updates := map[string]interface{}{ + "status": status, + "updated_at": time.Now(), + } + if sessionError != nil { + updates["error"] = sessionError + } + if endedAt != nil { + updates["ended_at"] = endedAt + } + return r.db.Model(&models.SummarizerSession{}).Where("id = ?", id).Updates(updates).Error +} + +func (r *SummarizerSessionRepository) FindByMeetingID(meetingID uint) ([]models.SummarizerSession, error) { + var sessions []models.SummarizerSession + err := r.db.Where("meeting_id = ?", meetingID).Order("created_at DESC").Find(&sessions).Error + return sessions, err +} + +func (r *SummarizerSessionRepository) FindStuck(status models.SummarizerSessionStatus, cutoffTime time.Time) ([]models.SummarizerSession, error) { + var sessions []models.SummarizerSession + err := r.db.Where("status = ? AND updated_at < ?", status, cutoffTime).Find(&sessions).Error + return sessions, err +} + +func (r *SummarizerSessionRepository) FindAllByUserIDPaginated(userID uint, page, pageSize int) ([]models.SummarizerSession, int64, error) { + var sessions []models.SummarizerSession + var total int64 + + query := r.db.Model(&models.SummarizerSession{}).Where("user_id = ?", userID) + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + offset := (page - 1) * pageSize + err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&sessions).Error + if err != nil { + return nil, 0, err + } + + return sessions, total, nil +} + +func (r *SummarizerSessionRepository) Delete(id uint) error { + return r.db.Delete(&models.SummarizerSession{}, id).Error +} + +func (r *SummarizerSessionRepository) UpdateTranscript(id uint, transcript string) error { + return r.db.Model(&models.SummarizerSession{}).Where("id = ?", id).Update("transcript", transcript).Error +} + +func (r *SummarizerSessionRepository) UpdateSummary(id uint, summary string) error { + return r.db.Model(&models.SummarizerSession{}).Where("id = ?", id).Update("summary", summary).Error +} diff --git a/backend/internal/repositories/transcript_repository.go b/backend/internal/repositories/transcript_repository.go new file mode 100644 index 0000000..1a0c8e9 --- /dev/null +++ b/backend/internal/repositories/transcript_repository.go @@ -0,0 +1,29 @@ +package repositories + +import ( + "mini-meeting/internal/models" + + "gorm.io/gorm" +) + +type TranscriptRepository struct { + db *gorm.DB +} + +func NewTranscriptRepository(db *gorm.DB) *TranscriptRepository { + return &TranscriptRepository{db: db} +} + +func (r *TranscriptRepository) Create(transcript *models.Transcript) error { + return r.db.Create(transcript).Error +} + +func (r *TranscriptRepository) FindBySessionID(sessionID uint) ([]models.Transcript, error) { + var transcripts []models.Transcript + err := r.db.Where("session_id = ?", sessionID).Order("start_time ASC").Find(&transcripts).Error + return transcripts, err +} + +func (r *TranscriptRepository) DeleteBySessionID(sessionID uint) error { + return r.db.Where("session_id = ?", sessionID).Delete(&models.Transcript{}).Error +} diff --git a/backend/internal/services/meeting_service.go b/backend/internal/services/meeting_service.go index 46a1cb5..ef8bbdc 100644 --- a/backend/internal/services/meeting_service.go +++ b/backend/internal/services/meeting_service.go @@ -125,3 +125,7 @@ func (s *MeetingService) DeleteMeeting(id uint, userID uint) error { return s.repo.Delete(id) } + +func (s *MeetingService) DeleteByCreatorID(creatorID uint) error { + return s.repo.DeleteByCreatorID(creatorID) +} diff --git a/backend/internal/services/normalization_service.go b/backend/internal/services/normalization_service.go index f5dbd4a..b83f87b 100644 --- a/backend/internal/services/normalization_service.go +++ b/backend/internal/services/normalization_service.go @@ -9,13 +9,15 @@ import ( ) type NormalizationService struct { - repo *repositories.SummarizerRepository + sessionRepo *repositories.SummarizerSessionRepository + transcriptRepo *repositories.TranscriptRepository summarizationService *SummarizationService } -func NewNormalizationService(repo *repositories.SummarizerRepository, summarizationService *SummarizationService) *NormalizationService { +func NewNormalizationService(sessionRepo *repositories.SummarizerSessionRepository, transcriptRepo *repositories.TranscriptRepository, summarizationService *SummarizationService) *NormalizationService { return &NormalizationService{ - repo: repo, + sessionRepo: sessionRepo, + transcriptRepo: transcriptRepo, summarizationService: summarizationService, } } @@ -31,7 +33,7 @@ type MergedSegment struct { // ProcessSession normalizes all transcripts for a given session func (s *NormalizationService) ProcessSession(sessionID uint) error { // 1. Get session and validate status - session, err := s.repo.FindSessionByID(sessionID) + session, err := s.sessionRepo.FindByID(sessionID) if err != nil { return fmt.Errorf("failed to get session: %w", err) } @@ -45,7 +47,7 @@ func (s *NormalizationService) ProcessSession(sessionID uint) error { } // 2. Get all transcripts for this session - transcripts, err := s.repo.FindTranscriptsBySessionID(sessionID) + transcripts, err := s.transcriptRepo.FindBySessionID(sessionID) if err != nil { return fmt.Errorf("failed to get transcripts: %w", err) } @@ -64,17 +66,17 @@ func (s *NormalizationService) ProcessSession(sessionID uint) error { // 5. Update session status to NORMALIZED now := time.Now() - if err := s.repo.UpdateSessionStatus(sessionID, models.StatusNormalized, nil, &now); err != nil { + if err := s.sessionRepo.UpdateStatus(sessionID, models.StatusNormalized, nil, &now); err != nil { return fmt.Errorf("failed to update session status: %w", err) } // 6. Save to session - if err := s.repo.UpdateSessionTranscript(sessionID, normalizedText); err != nil { + if err := s.sessionRepo.UpdateTranscript(sessionID, normalizedText); err != nil { return fmt.Errorf("failed to update session transcript: %w", err) } // 7. Delete individual transcript records - if err := s.repo.DeleteTranscriptsBySessionID(sessionID); err != nil { + if err := s.transcriptRepo.DeleteBySessionID(sessionID); err != nil { fmt.Printf("Warning: Failed to cleanup transcripts for session %d: %v\n", sessionID, err) } else { fmt.Printf("Cleaned up %d transcript records for session %d\n", len(transcripts), sessionID) diff --git a/backend/internal/services/summarization_service.go b/backend/internal/services/summarization_service.go index 340a3da..58466cc 100644 --- a/backend/internal/services/summarization_service.go +++ b/backend/internal/services/summarization_service.go @@ -25,23 +25,23 @@ type SummarizationResponse struct { } type SummarizationService struct { - repo *repositories.SummarizerRepository - userRepo *repositories.UserRepository + sessionRepo *repositories.SummarizerSessionRepository + userService *UserService openRouterService *OpenRouterService emailService *EmailService cfg *config.Config } func NewSummarizationService( - repo *repositories.SummarizerRepository, - userRepo *repositories.UserRepository, + sessionRepo *repositories.SummarizerSessionRepository, + userService *UserService, openRouterService *OpenRouterService, emailService *EmailService, cfg *config.Config, ) *SummarizationService { return &SummarizationService{ - repo: repo, - userRepo: userRepo, + sessionRepo: sessionRepo, + userService: userService, openRouterService: openRouterService, emailService: emailService, cfg: cfg, @@ -51,7 +51,7 @@ func NewSummarizationService( // ProcessSummarization generates an AI-powered summary from a normalized transcript func (s *SummarizationService) ProcessSummarization(sessionID uint) error { // 1. Get session and validate status - session, err := s.repo.FindSessionByID(sessionID) + session, err := s.sessionRepo.FindByID(sessionID) if err != nil { return fmt.Errorf("failed to get session: %w", err) } @@ -84,7 +84,7 @@ func (s *SummarizationService) ProcessSummarization(sessionID uint) error { if err != nil { // Update session with error errMsg := fmt.Sprintf("Failed to generate summary: %v", err) - s.repo.UpdateSessionStatus(sessionID, models.StatusNormalized, &errMsg, nil) + s.sessionRepo.UpdateStatus(sessionID, models.StatusNormalized, &errMsg, nil) return fmt.Errorf("failed to generate summary: %w", err) } @@ -93,12 +93,12 @@ func (s *SummarizationService) ProcessSummarization(sessionID uint) error { // 4. Update session status to SUMMARIZED now := time.Now() - if err := s.repo.UpdateSessionStatus(sessionID, models.StatusSummarized, nil, &now); err != nil { + if err := s.sessionRepo.UpdateStatus(sessionID, models.StatusSummarized, nil, &now); err != nil { return fmt.Errorf("failed to update session status: %w", err) } // 5. Save the summary to the session - if err := s.repo.UpdateSessionSummary(sessionID, resp.Summary); err != nil { + if err := s.sessionRepo.UpdateSummary(sessionID, resp.Summary); err != nil { return fmt.Errorf("failed to update session summary: %w", err) } @@ -107,7 +107,7 @@ func (s *SummarizationService) ProcessSummarization(sessionID uint) error { // 6. Fire email notification (non-blocking) go func() { - user, err := s.userRepo.FindByID(session.UserID) + user, err := s.userService.GetUserByID(session.UserID) if err != nil { fmt.Printf("EmailNotification: failed to fetch user for session %d: %v\n", sessionID, err) return diff --git a/backend/internal/services/summarizer_service.go b/backend/internal/services/summarizer_service.go index c017d02..390706b 100644 --- a/backend/internal/services/summarizer_service.go +++ b/backend/internal/services/summarizer_service.go @@ -19,9 +19,10 @@ import ( ) type SummarizerService struct { - repo *repositories.SummarizerRepository - meetingRepo *repositories.MeetingRepository - userRepo *repositories.UserRepository + sessionRepo *repositories.SummarizerSessionRepository + chunkRepo *repositories.AudioChunkRepository + transcriptRepo *repositories.TranscriptRepository + meetingService *MeetingService livekitService *LiveKitService transcriptionService *TranscriptionService cfg *config.Config @@ -35,17 +36,19 @@ type SummarizerService struct { } func NewSummarizerService( - repo *repositories.SummarizerRepository, - meetingRepo *repositories.MeetingRepository, - userRepo *repositories.UserRepository, + sessionRepo *repositories.SummarizerSessionRepository, + chunkRepo *repositories.AudioChunkRepository, + transcriptRepo *repositories.TranscriptRepository, + meetingService *MeetingService, livekitService *LiveKitService, transcriptionService *TranscriptionService, cfg *config.Config, ) *SummarizerService { return &SummarizerService{ - repo: repo, - meetingRepo: meetingRepo, - userRepo: userRepo, + sessionRepo: sessionRepo, + chunkRepo: chunkRepo, + transcriptRepo: transcriptRepo, + meetingService: meetingService, livekitService: livekitService, transcriptionService: transcriptionService, cfg: cfg, @@ -56,7 +59,7 @@ func NewSummarizerService( // StartSummarizer starts a new summarizer session for a meeting func (s *SummarizerService) StartSummarizer(meetingID uint, userID uint) (*models.SummarizerSession, error) { // Validate meeting exists and user is the creator - meeting, err := s.meetingRepo.FindByID(meetingID) + meeting, err := s.meetingService.GetMeetingByID(meetingID) if err != nil { return nil, fmt.Errorf("meeting not found: %w", err) } @@ -66,7 +69,7 @@ func (s *SummarizerService) StartSummarizer(meetingID uint, userID uint) (*model } // Check if there's already an active session - existingSession, err := s.repo.FindActiveSessionByMeetingID(meetingID) + existingSession, err := s.sessionRepo.FindActiveByMeetingID(meetingID) if err == nil && existingSession != nil { return nil, fmt.Errorf("summarizer already running for this meeting") } @@ -79,7 +82,7 @@ func (s *SummarizerService) StartSummarizer(meetingID uint, userID uint) (*model StartedAt: time.Now(), } - if err := s.repo.CreateSession(session); err != nil { + if err := s.sessionRepo.Create(session); err != nil { return nil, fmt.Errorf("failed to create session: %w", err) } @@ -89,7 +92,7 @@ func (s *SummarizerService) StartSummarizer(meetingID uint, userID uint) (*model // Update session status with error now := time.Now() errMsg := fmt.Sprintf("Failed to join LiveKit room: %v", err) - s.repo.UpdateSessionStatus(session.ID, models.StatusStarted, &errMsg, &now) + s.sessionRepo.UpdateStatus(session.ID, models.StatusStarted, &errMsg, &now) fmt.Printf("%s\n", errMsg) } }() @@ -100,13 +103,13 @@ func (s *SummarizerService) StartSummarizer(meetingID uint, userID uint) (*model // StopSummarizer stops an active summarizer session func (s *SummarizerService) StopSummarizer(sessionID uint, userID uint) (int64, error) { // Get session - session, err := s.repo.FindSessionByID(sessionID) + session, err := s.sessionRepo.FindByID(sessionID) if err != nil { return 0, fmt.Errorf("session not found: %w", err) } // Validate user is meeting creator - meeting, err := s.meetingRepo.FindByID(session.MeetingID) + meeting, err := s.meetingService.GetMeetingByID(session.MeetingID) if err != nil { return 0, fmt.Errorf("meeting not found: %w", err) } @@ -133,12 +136,12 @@ func (s *SummarizerService) StopSummarizer(sessionID uint, userID uint) (int64, // Update session status to CAPTURED now := time.Now() - if err := s.repo.UpdateSessionStatus(sessionID, models.StatusCaptured, nil, &now); err != nil { + if err := s.sessionRepo.UpdateStatus(sessionID, models.StatusCaptured, nil, &now); err != nil { return 0, fmt.Errorf("failed to update session status: %w", err) } // Count total chunks - totalChunks, err := s.repo.CountChunksBySessionID(sessionID) + totalChunks, err := s.chunkRepo.CountBySessionID(sessionID) if err != nil { return 0, fmt.Errorf("failed to count chunks: %w", err) } @@ -326,7 +329,7 @@ func (s *SummarizerService) createChunkMetadata(sessionID uint, userIdentity str DurationSeconds: duration, } - if err := s.repo.CreateAudioChunk(chunk); err != nil { + if err := s.chunkRepo.Create(chunk); err != nil { return fmt.Errorf("failed to create audio chunk metadata: %w", err) } @@ -336,7 +339,7 @@ func (s *SummarizerService) createChunkMetadata(sessionID uint, userIdentity str // GetActiveSession returns the active summarizer session for a meeting func (s *SummarizerService) GetActiveSession(meetingID uint) (*models.SummarizerSession, error) { - session, err := s.repo.FindActiveSessionByMeetingID(meetingID) + session, err := s.sessionRepo.FindActiveByMeetingID(meetingID) if err != nil { return nil, fmt.Errorf("no active summarizer session found") } @@ -345,7 +348,7 @@ func (s *SummarizerService) GetActiveSession(meetingID uint) (*models.Summarizer // GetSessionByID returns a session by its ID func (s *SummarizerService) GetSessionByID(sessionID uint) (*models.SummarizerSession, error) { - return s.repo.FindSessionByID(sessionID) + return s.sessionRepo.FindByID(sessionID) } // GetSessions returns a paginated list of sessions for a user @@ -358,7 +361,7 @@ func (s *SummarizerService) GetSessions(userID uint, page, pageSize int) (*types pageSize = 10 } - sessions, total, err := s.repo.FindAllByUserIDPaginated(userID, page, pageSize) + sessions, total, err := s.sessionRepo.FindAllByUserIDPaginated(userID, page, pageSize) if err != nil { return nil, fmt.Errorf("failed to fetch sessions: %w", err) } @@ -391,7 +394,7 @@ func (s *SummarizerService) GetSessions(userID uint, page, pageSize int) (*types // GetSession retrieves a specific session for a user (verifying ownership) func (s *SummarizerService) GetSession(sessionID, userID uint) (*types.SessionResponse, error) { - session, err := s.repo.FindSessionByID(sessionID) + session, err := s.sessionRepo.FindByID(sessionID) if err != nil { return nil, fmt.Errorf("session not found: %w", err) } @@ -413,7 +416,7 @@ func (s *SummarizerService) GetSession(sessionID, userID uint) (*types.SessionRe // DeleteSession deletes a session (verifying ownership) func (s *SummarizerService) DeleteSession(sessionID, userID uint) error { - session, err := s.repo.FindSessionByID(sessionID) + session, err := s.sessionRepo.FindByID(sessionID) if err != nil { return fmt.Errorf("session not found: %w", err) } @@ -429,17 +432,17 @@ func (s *SummarizerService) DeleteSession(sessionID, userID uint) error { } // 2. Delete chunks for this session in database - if err := s.repo.DeleteChunksBySessionID(sessionID); err != nil { + if err := s.chunkRepo.DeleteBySessionID(sessionID); err != nil { return fmt.Errorf("failed to delete chunks: %w", err) } // 3. Delete transcripts for this session in database - if err := s.repo.DeleteTranscriptsBySessionID(sessionID); err != nil { + if err := s.transcriptRepo.DeleteBySessionID(sessionID); err != nil { return fmt.Errorf("failed to delete transcripts: %w", err) } // 4. Finally delete the session record itself - if err := s.repo.DeleteSession(sessionID); err != nil { + if err := s.sessionRepo.Delete(sessionID); err != nil { return fmt.Errorf("failed to delete session: %w", err) } diff --git a/backend/internal/services/transcription_service.go b/backend/internal/services/transcription_service.go index d389ad1..ff07ac8 100644 --- a/backend/internal/services/transcription_service.go +++ b/backend/internal/services/transcription_service.go @@ -16,14 +16,18 @@ import ( ) type TranscriptionService struct { - repo *repositories.SummarizerRepository + sessionRepo *repositories.SummarizerSessionRepository + chunkRepo *repositories.AudioChunkRepository + transcriptRepo *repositories.TranscriptRepository normalizationService *NormalizationService cfg *config.Config } -func NewTranscriptionService(repo *repositories.SummarizerRepository, normalizationService *NormalizationService, cfg *config.Config) *TranscriptionService { +func NewTranscriptionService(sessionRepo *repositories.SummarizerSessionRepository, chunkRepo *repositories.AudioChunkRepository, transcriptRepo *repositories.TranscriptRepository, normalizationService *NormalizationService, cfg *config.Config) *TranscriptionService { return &TranscriptionService{ - repo: repo, + sessionRepo: sessionRepo, + chunkRepo: chunkRepo, + transcriptRepo: transcriptRepo, normalizationService: normalizationService, cfg: cfg, } @@ -101,7 +105,7 @@ func (s *TranscriptionService) TranscribeChunk(filePath string) (string, error) // ProcessSession transcribes all chunks for a given session func (s *TranscriptionService) ProcessSession(sessionID uint) error { // 1. Get session and validate status - session, err := s.repo.FindSessionByID(sessionID) + session, err := s.sessionRepo.FindByID(sessionID) if err != nil { return fmt.Errorf("failed to get session: %w", err) } @@ -115,7 +119,7 @@ func (s *TranscriptionService) ProcessSession(sessionID uint) error { } // 2. Get all audio chunks - chunks, err := s.repo.FindChunksBySessionID(sessionID) + chunks, err := s.chunkRepo.FindBySessionID(sessionID) if err != nil { return fmt.Errorf("failed to get chunks: %w", err) } @@ -123,14 +127,14 @@ func (s *TranscriptionService) ProcessSession(sessionID uint) error { if len(chunks) == 0 { errMsg := "Session stopped immediately — no audio chunks were captured" now := time.Now() - s.repo.UpdateSessionStatus(sessionID, models.StatusCaptured, &errMsg, &now) + s.sessionRepo.UpdateStatus(sessionID, models.StatusCaptured, &errMsg, &now) return fmt.Errorf("%s", errMsg) } if len(chunks) <= 2 { errMsg := fmt.Sprintf("Session stopped too quickly — only %d audio chunk(s) captured, not enough speech to transcribe", len(chunks)) now := time.Now() - s.repo.UpdateSessionStatus(sessionID, models.StatusCaptured, &errMsg, &now) + s.sessionRepo.UpdateStatus(sessionID, models.StatusCaptured, &errMsg, &now) return fmt.Errorf("%s", errMsg) } @@ -164,7 +168,7 @@ func (s *TranscriptionService) ProcessSession(sessionID uint) error { EndTime: chunk.DurationSeconds * float64(chunk.ChunkIndex+1), } - if err := s.repo.CreateTranscript(transcript); err != nil { + if err := s.transcriptRepo.Create(transcript); err != nil { fmt.Printf("Failed to save transcript for chunk %d: %v\n", chunk.ID, err) continue } @@ -176,12 +180,12 @@ func (s *TranscriptionService) ProcessSession(sessionID uint) error { if successCount == 0 { errMsg := "Failed to transcribe any chunks — all audio segments were empty or unreadable" now := time.Now() - s.repo.UpdateSessionStatus(sessionID, models.StatusCaptured, &errMsg, &now) + s.sessionRepo.UpdateStatus(sessionID, models.StatusCaptured, &errMsg, &now) return fmt.Errorf("%s", errMsg) } now := time.Now() - if err := s.repo.UpdateSessionStatus(sessionID, models.StatusTranscribed, nil, &now); err != nil { + if err := s.sessionRepo.UpdateStatus(sessionID, models.StatusTranscribed, nil, &now); err != nil { return fmt.Errorf("failed to update session status: %w", err) } @@ -195,7 +199,7 @@ func (s *TranscriptionService) ProcessSession(sessionID uint) error { fmt.Printf("Cleaned up audio files for session %d\n", sessionID) } - if err := s.repo.DeleteChunksBySessionID(sessionID); err != nil { + if err := s.chunkRepo.DeleteBySessionID(sessionID); err != nil { fmt.Printf("Warning: Failed to cleanup session chunks from database %d: %v\n", sessionID, err) } else { fmt.Printf("Cleaned up audio chunks records from database for session %d\n", sessionID) diff --git a/backend/internal/services/user_service.go b/backend/internal/services/user_service.go index 6151655..b23685e 100644 --- a/backend/internal/services/user_service.go +++ b/backend/internal/services/user_service.go @@ -11,14 +11,14 @@ import ( ) type UserService struct { - repo *repositories.UserRepository - meetingRepo *repositories.MeetingRepository + repo *repositories.UserRepository + meetingService *MeetingService } -func NewUserService(repo *repositories.UserRepository, meetingRepo *repositories.MeetingRepository) *UserService { +func NewUserService(repo *repositories.UserRepository, meetingService *MeetingService) *UserService { return &UserService{ - repo: repo, - meetingRepo: meetingRepo, + repo: repo, + meetingService: meetingService, } } @@ -117,7 +117,7 @@ func (s *UserService) DeleteUser(id uint) error { } // Delete all meetings created by this user - if err := s.meetingRepo.DeleteByCreatorID(id); err != nil { + if err := s.meetingService.DeleteByCreatorID(id); err != nil { return err } diff --git a/backend/internal/workers/normalization_worker.go b/backend/internal/workers/normalization_worker.go index b0b3627..d2d1f84 100644 --- a/backend/internal/workers/normalization_worker.go +++ b/backend/internal/workers/normalization_worker.go @@ -9,14 +9,14 @@ import ( ) type NormalizationWorker struct { - repo *repositories.SummarizerRepository + repo *repositories.SummarizerSessionRepository normalizationService *services.NormalizationService interval time.Duration stuckThreshold time.Duration } func NewNormalizationWorker( - repo *repositories.SummarizerRepository, + repo *repositories.SummarizerSessionRepository, normalizationService *services.NormalizationService, interval time.Duration, stuckThreshold time.Duration, @@ -44,7 +44,7 @@ func (w *NormalizationWorker) processStuckSessions() { cutoffTime := time.Now().Add(-w.stuckThreshold) // Find sessions that are TRANSCRIBED but haven't been updated recently - sessions, err := w.repo.FindStuckSessions(models.StatusTranscribed, cutoffTime) + sessions, err := w.repo.FindStuck(models.StatusTranscribed, cutoffTime) if err != nil { fmt.Printf("NormalizationWorker: Error finding stuck sessions: %v\n", err) return diff --git a/backend/internal/workers/summarization_worker.go b/backend/internal/workers/summarization_worker.go index 9c4f9e0..716ef76 100644 --- a/backend/internal/workers/summarization_worker.go +++ b/backend/internal/workers/summarization_worker.go @@ -9,14 +9,14 @@ import ( ) type SummarizationWorker struct { - repo *repositories.SummarizerRepository + repo *repositories.SummarizerSessionRepository summarizationService *services.SummarizationService interval time.Duration stuckThreshold time.Duration } func NewSummarizationWorker( - repo *repositories.SummarizerRepository, + repo *repositories.SummarizerSessionRepository, summarizationService *services.SummarizationService, interval time.Duration, stuckThreshold time.Duration, @@ -44,7 +44,7 @@ func (w *SummarizationWorker) processStuckSessions() { cutoffTime := time.Now().Add(-w.stuckThreshold) // Find sessions that are NORMALIZED but haven't been updated recently - sessions, err := w.repo.FindStuckSessions(models.StatusNormalized, cutoffTime) + sessions, err := w.repo.FindStuck(models.StatusNormalized, cutoffTime) if err != nil { fmt.Printf("SummarizationWorker: Error finding stuck sessions: %v\n", err) return diff --git a/backend/internal/workers/transcription_worker.go b/backend/internal/workers/transcription_worker.go index 009b45d..800b6f8 100644 --- a/backend/internal/workers/transcription_worker.go +++ b/backend/internal/workers/transcription_worker.go @@ -9,14 +9,14 @@ import ( ) type TranscriptionWorker struct { - repo *repositories.SummarizerRepository + repo *repositories.SummarizerSessionRepository transcriptionService *services.TranscriptionService interval time.Duration stuckThreshold time.Duration } func NewTranscriptionWorker( - repo *repositories.SummarizerRepository, + repo *repositories.SummarizerSessionRepository, transcriptionService *services.TranscriptionService, interval time.Duration, stuckThreshold time.Duration, @@ -47,7 +47,7 @@ func (w *TranscriptionWorker) Start() { func (w *TranscriptionWorker) processStuckSessions() { cutoffTime := time.Now().Add(-w.stuckThreshold) - sessions, err := w.repo.FindStuckSessions(models.StatusCaptured, cutoffTime) + sessions, err := w.repo.FindStuck(models.StatusCaptured, cutoffTime) if err != nil { log.Printf("Worker: Failed to fetch stuck sessions: %v", err) return From e9e1def9b64126c3abd43915573c0903152d7918 Mon Sep 17 00:00:00 2001 From: Mohamed Ramadan Date: Fri, 6 Mar 2026 23:18:56 +0200 Subject: [PATCH 2/5] refactor: migrate internal/types to internal/handlers/dto - Create internal/handlers/dto package with five files: meeting.go, user.go, livekit.go, lobby.go, summarizer.go - Move all HTTP DTOs and mapper functions from internal/types into dto - Update handlers (livekit, lobby, meeting, user) to import from dto - Update services (user, summarizer) to import from dto - Delete internal/types package --- .../{types => handlers/dto}/livekit.go | 2 +- .../internal/{types => handlers/dto}/lobby.go | 3 +-- .../{types => handlers/dto}/meeting.go | 2 +- .../{types => handlers/dto}/summarizer.go | 2 +- .../internal/{types => handlers/dto}/user.go | 2 +- backend/internal/handlers/livekit_handler.go | 18 +++++++++--------- backend/internal/handlers/lobby_handler.go | 10 +++++----- backend/internal/handlers/meeting_handler.go | 16 ++++++++-------- backend/internal/handlers/user_handler.go | 4 ++-- .../internal/services/summarizer_service.go | 14 +++++++------- backend/internal/services/user_service.go | 8 ++++---- 11 files changed, 40 insertions(+), 41 deletions(-) rename backend/internal/{types => handlers/dto}/livekit.go (99%) rename backend/internal/{types => handlers/dto}/lobby.go (98%) rename backend/internal/{types => handlers/dto}/meeting.go (98%) rename backend/internal/{types => handlers/dto}/summarizer.go (98%) rename backend/internal/{types => handlers/dto}/user.go (98%) diff --git a/backend/internal/types/livekit.go b/backend/internal/handlers/dto/livekit.go similarity index 99% rename from backend/internal/types/livekit.go rename to backend/internal/handlers/dto/livekit.go index be81133..a1ee834 100644 --- a/backend/internal/types/livekit.go +++ b/backend/internal/handlers/dto/livekit.go @@ -1,4 +1,4 @@ -package types +package dto // GenerateTokenRequest represents the request to generate a LiveKit token type GenerateTokenRequest struct { diff --git a/backend/internal/types/lobby.go b/backend/internal/handlers/dto/lobby.go similarity index 98% rename from backend/internal/types/lobby.go rename to backend/internal/handlers/dto/lobby.go index a3df289..5c4fada 100644 --- a/backend/internal/types/lobby.go +++ b/backend/internal/handlers/dto/lobby.go @@ -1,4 +1,4 @@ -package types +package dto // LobbyJoinRequest represents a request to join a meeting lobby type LobbyJoinRequest struct { @@ -25,4 +25,3 @@ type LobbyRespondRequest struct { RequestID string `json:"request_id" validate:"required"` Action string `json:"action" validate:"required"` // "approve" or "reject" } - diff --git a/backend/internal/types/meeting.go b/backend/internal/handlers/dto/meeting.go similarity index 98% rename from backend/internal/types/meeting.go rename to backend/internal/handlers/dto/meeting.go index 931a0e7..8e5828b 100644 --- a/backend/internal/types/meeting.go +++ b/backend/internal/handlers/dto/meeting.go @@ -1,4 +1,4 @@ -package types +package dto import ( "mini-meeting/internal/models" diff --git a/backend/internal/types/summarizer.go b/backend/internal/handlers/dto/summarizer.go similarity index 98% rename from backend/internal/types/summarizer.go rename to backend/internal/handlers/dto/summarizer.go index d9cc247..d69bbf3 100644 --- a/backend/internal/types/summarizer.go +++ b/backend/internal/handlers/dto/summarizer.go @@ -1,4 +1,4 @@ -package types +package dto import ( "mini-meeting/internal/models" diff --git a/backend/internal/types/user.go b/backend/internal/handlers/dto/user.go similarity index 98% rename from backend/internal/types/user.go rename to backend/internal/handlers/dto/user.go index 0033399..b0c8f73 100644 --- a/backend/internal/types/user.go +++ b/backend/internal/handlers/dto/user.go @@ -1,4 +1,4 @@ -package types +package dto import "mini-meeting/internal/models" diff --git a/backend/internal/handlers/livekit_handler.go b/backend/internal/handlers/livekit_handler.go index d1742a4..5e3473d 100644 --- a/backend/internal/handlers/livekit_handler.go +++ b/backend/internal/handlers/livekit_handler.go @@ -5,8 +5,8 @@ import ( "strings" "mini-meeting/internal/config" + "mini-meeting/internal/handlers/dto" "mini-meeting/internal/services" - "mini-meeting/internal/types" "mini-meeting/pkg/utils" "github.com/gofiber/fiber/v2" @@ -56,7 +56,7 @@ func (h *LiveKitHandler) GenerateToken(c *fiber.Ctx) error { } } - var req types.GenerateTokenRequest + var req dto.GenerateTokenRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", @@ -131,7 +131,7 @@ func (h *LiveKitHandler) GenerateToken(c *fiber.Ctx) error { }) } - response := types.GenerateTokenResponse{ + response := dto.GenerateTokenResponse{ Token: token, URL: h.livekitService.GetURL(), RoomCode: req.MeetingCode, @@ -151,7 +151,7 @@ func (h *LiveKitHandler) RemoveParticipant(c *fiber.Ctx) error { }) } - var req types.RemoveParticipantRequest + var req dto.RemoveParticipantRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", @@ -210,9 +210,9 @@ func (h *LiveKitHandler) ListParticipants(c *fiber.Ctx) error { } // Convert to response format - participantInfos := make([]types.ParticipantInfo, 0, len(participants)) + participantInfos := make([]dto.ParticipantInfo, 0, len(participants)) for _, p := range participants { - participantInfos = append(participantInfos, types.ParticipantInfo{ + participantInfos = append(participantInfos, dto.ParticipantInfo{ Identity: p.Identity, Name: p.Name, State: p.State.String(), @@ -221,7 +221,7 @@ func (h *LiveKitHandler) ListParticipants(c *fiber.Ctx) error { }) } - response := types.ListParticipantsResponse{ + response := dto.ListParticipantsResponse{ Participants: participantInfos, } @@ -266,7 +266,7 @@ func (h *LiveKitHandler) MuteParticipant(c *fiber.Ctx) error { }) } - var req types.MuteParticipantRequest + var req dto.MuteParticipantRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", @@ -308,7 +308,7 @@ func (h *LiveKitHandler) EndMeeting(c *fiber.Ctx) error { }) } - var req types.EndMeetingRequest + var req dto.EndMeetingRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", diff --git a/backend/internal/handlers/lobby_handler.go b/backend/internal/handlers/lobby_handler.go index 06ec9bd..71c574b 100644 --- a/backend/internal/handlers/lobby_handler.go +++ b/backend/internal/handlers/lobby_handler.go @@ -7,8 +7,8 @@ import ( "mini-meeting/internal/cache" "mini-meeting/internal/config" + "mini-meeting/internal/handlers/dto" "mini-meeting/internal/services" - "mini-meeting/internal/types" "mini-meeting/pkg/utils" "github.com/gofiber/fiber/v2" @@ -41,7 +41,7 @@ func NewLobbyHandler( // Otherwise, a pending request is created in the lobby cache. // POST /api/v1/lobby/request func (h *LobbyHandler) RequestToJoin(c *fiber.Ctx) error { - var req types.LobbyJoinRequest + var req dto.LobbyJoinRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", @@ -123,7 +123,7 @@ func (h *LobbyHandler) RequestToJoin(c *fiber.Ctx) error { }) } - return c.JSON(types.LobbyJoinResponse{ + return c.JSON(dto.LobbyJoinResponse{ RequestID: "", Status: "auto_approved", Token: token, @@ -158,7 +158,7 @@ func (h *LobbyHandler) RequestToJoin(c *fiber.Ctx) error { // Notify admins via WebSocket NotifyAdminsOfNewRequest(lobbyReq) - return c.JSON(types.LobbyJoinResponse{ + return c.JSON(dto.LobbyJoinResponse{ RequestID: requestID, Status: "pending", }) @@ -206,7 +206,7 @@ func (h *LobbyHandler) RespondToRequest(c *fiber.Ctx) error { }) } - var req types.LobbyRespondRequest + var req dto.LobbyRespondRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", diff --git a/backend/internal/handlers/meeting_handler.go b/backend/internal/handlers/meeting_handler.go index 92ab6ed..2e7cf81 100644 --- a/backend/internal/handlers/meeting_handler.go +++ b/backend/internal/handlers/meeting_handler.go @@ -2,8 +2,8 @@ package handlers import ( "mini-meeting/internal/config" + "mini-meeting/internal/handlers/dto" "mini-meeting/internal/services" - "mini-meeting/internal/types" "strconv" "github.com/gofiber/fiber/v2" @@ -45,7 +45,7 @@ func (h *MeetingHandler) CreateMeeting(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(fiber.Map{ "message": "Meeting created successfully", - "data": types.ToMeetingResponse(meeting, baseURL), + "data": dto.ToMeetingResponse(meeting, baseURL), }) } @@ -77,7 +77,7 @@ func (h *MeetingHandler) GetMeeting(c *fiber.Ctx) error { } return c.JSON(fiber.Map{ - "data": types.ToMeetingResponse(meeting, baseURL), + "data": dto.ToMeetingResponse(meeting, baseURL), }) } @@ -109,7 +109,7 @@ func (h *MeetingHandler) GetMeetingByCode(c *fiber.Ctx) error { } return c.JSON(fiber.Map{ - "data": types.ToMeetingResponse(meeting, baseURL), + "data": dto.ToMeetingResponse(meeting, baseURL), }) } @@ -135,9 +135,9 @@ func (h *MeetingHandler) GetMyMeetings(c *fiber.Ctx) error { baseURL = "http://localhost:5173" } - responses := make([]types.MeetingResponse, len(meetings)) + responses := make([]dto.MeetingResponse, len(meetings)) for i, meeting := range meetings { - responses[i] = types.ToMeetingResponse(&meeting, baseURL) + responses[i] = dto.ToMeetingResponse(&meeting, baseURL) } return c.JSON(fiber.Map{ @@ -160,9 +160,9 @@ func (h *MeetingHandler) GetAllMeetings(c *fiber.Ctx) error { baseURL = "http://localhost:5173" } - responses := make([]types.MeetingResponse, len(meetings)) + responses := make([]dto.MeetingResponse, len(meetings)) for i, meeting := range meetings { - responses[i] = types.ToMeetingResponse(&meeting, baseURL) + responses[i] = dto.ToMeetingResponse(&meeting, baseURL) } return c.JSON(fiber.Map{ diff --git a/backend/internal/handlers/user_handler.go b/backend/internal/handlers/user_handler.go index c5eb810..b098834 100644 --- a/backend/internal/handlers/user_handler.go +++ b/backend/internal/handlers/user_handler.go @@ -1,8 +1,8 @@ package handlers import ( + "mini-meeting/internal/handlers/dto" "mini-meeting/internal/services" - "mini-meeting/internal/types" "strconv" "github.com/gofiber/fiber/v2" @@ -110,7 +110,7 @@ func (h *UserHandler) UpdateMe(c *fiber.Ctx) error { }) } - var req types.UpdateUserRequest + var req dto.UpdateUserRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", diff --git a/backend/internal/services/summarizer_service.go b/backend/internal/services/summarizer_service.go index 390706b..3c4a830 100644 --- a/backend/internal/services/summarizer_service.go +++ b/backend/internal/services/summarizer_service.go @@ -5,9 +5,9 @@ import ( "fmt" "io" "mini-meeting/internal/config" + "mini-meeting/internal/handlers/dto" "mini-meeting/internal/models" "mini-meeting/internal/repositories" - "mini-meeting/internal/types" "os" "path/filepath" "sync" @@ -352,7 +352,7 @@ func (s *SummarizerService) GetSessionByID(sessionID uint) (*models.SummarizerSe } // GetSessions returns a paginated list of sessions for a user -func (s *SummarizerService) GetSessions(userID uint, page, pageSize int) (*types.PaginatedSessionsResponse, error) { +func (s *SummarizerService) GetSessions(userID uint, page, pageSize int) (*dto.PaginatedSessionsResponse, error) { // Set default values if page < 1 { page = 1 @@ -367,9 +367,9 @@ func (s *SummarizerService) GetSessions(userID uint, page, pageSize int) (*types } // Map models to response type - sessionList := make([]types.SessionsList, len(sessions)) + sessionList := make([]dto.SessionsList, len(sessions)) for i, session := range sessions { - sessionList[i] = types.SessionsList{ + sessionList[i] = dto.SessionsList{ ID: session.ID, Status: session.Status, Error: session.Error, @@ -383,7 +383,7 @@ func (s *SummarizerService) GetSessions(userID uint, page, pageSize int) (*types totalPages++ } - return &types.PaginatedSessionsResponse{ + return &dto.PaginatedSessionsResponse{ Data: sessionList, Total: total, Page: page, @@ -393,7 +393,7 @@ func (s *SummarizerService) GetSessions(userID uint, page, pageSize int) (*types } // GetSession retrieves a specific session for a user (verifying ownership) -func (s *SummarizerService) GetSession(sessionID, userID uint) (*types.SessionResponse, error) { +func (s *SummarizerService) GetSession(sessionID, userID uint) (*dto.SessionResponse, error) { session, err := s.sessionRepo.FindByID(sessionID) if err != nil { return nil, fmt.Errorf("session not found: %w", err) @@ -403,7 +403,7 @@ func (s *SummarizerService) GetSession(sessionID, userID uint) (*types.SessionRe return nil, errors.New("unauthorized: session does not belong to user") } - return &types.SessionResponse{ + return &dto.SessionResponse{ ID: session.ID, Status: session.Status, Error: session.Error, diff --git a/backend/internal/services/user_service.go b/backend/internal/services/user_service.go index b23685e..38d878d 100644 --- a/backend/internal/services/user_service.go +++ b/backend/internal/services/user_service.go @@ -2,9 +2,9 @@ package services import ( "errors" + "mini-meeting/internal/handlers/dto" "mini-meeting/internal/models" "mini-meeting/internal/repositories" - "mini-meeting/internal/types" "strings" "gorm.io/gorm" @@ -45,7 +45,7 @@ func (s *UserService) GetUserByEmail(email string) (*models.User, error) { return user, nil } -func (s *UserService) GetAllUsersPaginated(page, pageSize int, search string) (*types.PaginatedUsersResponse, error) { +func (s *UserService) GetAllUsersPaginated(page, pageSize int, search string) (*dto.PaginatedUsersResponse, error) { // Set default values if page < 1 { page = 1 @@ -65,7 +65,7 @@ func (s *UserService) GetAllUsersPaginated(page, pageSize int, search string) (* totalPages++ } - return &types.PaginatedUsersResponse{ + return &dto.PaginatedUsersResponse{ Data: users, Total: total, Page: page, @@ -74,7 +74,7 @@ func (s *UserService) GetAllUsersPaginated(page, pageSize int, search string) (* }, nil } -func (s *UserService) UpdateUser(id uint, req *types.UpdateUserRequest) (*models.User, error) { +func (s *UserService) UpdateUser(id uint, req *dto.UpdateUserRequest) (*models.User, error) { user, err := s.repo.FindByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { From ec301eff8b88b752d554660bc994b9d9986dd1b4 Mon Sep 17 00:00:00 2001 From: Mohamed Ramadan Date: Fri, 6 Mar 2026 23:54:30 +0200 Subject: [PATCH 3/5] refactor: split routes into domain files and remove unused API endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Break routes.go into auth.go, user.go, meeting.go, livekit.go, lobby.go (same package, single SetupRoutes entry point delegates to each) - Remove POST /lobby/respond (HTTP fallback) — lobby approval is handled exclusively over WebSocket; delete RespondToRequest handler, LobbyRespondRequest DTO, and related route/middleware wiring - Remove frontend createUser / updateUser from userAdminService — these called POST /users and PATCH /users/:id which never existed in the backend; drop unused CreateUserRequest, UpdateUserRequest, and UserUpdateResponse types --- backend/internal/handlers/dto/lobby.go | 7 - backend/internal/handlers/lobby_handler.go | 124 ------------------ backend/internal/routes/auth.go | 13 ++ backend/internal/routes/livekit.go | 23 ++++ backend/internal/routes/lobby.go | 32 +++++ backend/internal/routes/meeting.go | 40 ++++++ backend/internal/routes/routes.go | 89 +++---------- backend/internal/routes/user.go | 22 ++++ frontend/src/services/api/user/index.ts | 2 - .../services/api/user/user-admin.service.ts | 22 ---- frontend/src/types/user.types.ts | 21 --- 11 files changed, 146 insertions(+), 249 deletions(-) create mode 100644 backend/internal/routes/auth.go create mode 100644 backend/internal/routes/livekit.go create mode 100644 backend/internal/routes/lobby.go create mode 100644 backend/internal/routes/meeting.go create mode 100644 backend/internal/routes/user.go diff --git a/backend/internal/handlers/dto/lobby.go b/backend/internal/handlers/dto/lobby.go index 5c4fada..6cc127f 100644 --- a/backend/internal/handlers/dto/lobby.go +++ b/backend/internal/handlers/dto/lobby.go @@ -18,10 +18,3 @@ type LobbyJoinResponse struct { Identity string `json:"identity,omitempty"` UserName string `json:"user_name,omitempty"` } - -// LobbyRespondRequest is sent by admin to approve/reject a request (HTTP fallback) -type LobbyRespondRequest struct { - MeetingCode string `json:"meeting_code" validate:"required"` - RequestID string `json:"request_id" validate:"required"` - Action string `json:"action" validate:"required"` // "approve" or "reject" -} diff --git a/backend/internal/handlers/lobby_handler.go b/backend/internal/handlers/lobby_handler.go index 71c574b..2c95883 100644 --- a/backend/internal/handlers/lobby_handler.go +++ b/backend/internal/handlers/lobby_handler.go @@ -195,127 +195,3 @@ func (h *LobbyHandler) CancelRequest(c *fiber.Ctx) error { "message": "Request cancelled", }) } - -// RespondToRequest lets the admin approve or reject a lobby request (HTTP fallback). -// POST /api/v1/lobby/respond -func (h *LobbyHandler) RespondToRequest(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(uint) - if !ok { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": "Unauthorized", - }) - } - - var req dto.LobbyRespondRequest - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request body", - }) - } - - if req.MeetingCode == "" || req.RequestID == "" || req.Action == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "meeting_code, request_id, and action are required", - }) - } - - if req.Action != "approve" && req.Action != "reject" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "action must be 'approve' or 'reject'", - }) - } - - // Verify the user is the meeting creator - meeting, err := h.meetingService.GetMeetingByCode(req.MeetingCode) - if err != nil { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": "Meeting not found", - }) - } - - if meeting.CreatorID != userID { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ - "error": "Only meeting creator can manage lobby requests", - }) - } - - // Get the lobby request - lobbyReq, err := cache.GetLobbyRequest(req.RequestID) - if err != nil { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": "Request not found or expired", - }) - } - - if lobbyReq.MeetingCode != req.MeetingCode { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Request does not belong to this meeting", - }) - } - - if req.Action == "reject" { - if err := cache.UpdateLobbyRequestStatus(req.RequestID, cache.LobbyStatusRejected); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Failed to reject request", - }) - } - - // Notify visitor via WebSocket - cache.Hub.NotifyVisitor(req.RequestID, map[string]string{"type": "rejected"}) - cache.Hub.NotifyAdmins(req.MeetingCode, map[string]string{ - "type": "request_resolved", - "request_id": req.RequestID, - }) - go cache.CleanupLobbyRequest(req.RequestID) - - return c.JSON(fiber.Map{ - "message": "Request rejected", - }) - } - - // Approve: generate token and store it - metadata := fmt.Sprintf(`{"name":"%s","avatar":"%s","role":"%s"}`, lobbyReq.Name, lobbyReq.AvatarURL, lobbyReq.Role) - - token, err := h.livekitService.CreateJoinToken( - lobbyReq.MeetingCode, - lobbyReq.Identity, - lobbyReq.Name, - lobbyReq.Role, - metadata, - ) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Failed to generate token", - }) - } - - // Update status to approved - if err := cache.UpdateLobbyRequestStatus(req.RequestID, cache.LobbyStatusApproved); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Failed to approve request", - }) - } - - // Notify visitor via WebSocket - cache.Hub.NotifyVisitor(req.RequestID, map[string]interface{}{ - "type": "approved", - "token": token, - "url": h.livekitService.GetURL(), - "room_code": lobbyReq.MeetingCode, - "identity": lobbyReq.Identity, - "user_name": lobbyReq.Name, - }) - cache.Hub.NotifyAdmins(req.MeetingCode, map[string]string{ - "type": "request_resolved", - "request_id": req.RequestID, - }) - go func() { - // give visitor time to receive the token - time.Sleep(5 * time.Second) - cache.CleanupLobbyRequest(req.RequestID) - }() - - return c.JSON(fiber.Map{ - "message": "Request approved", - }) -} diff --git a/backend/internal/routes/auth.go b/backend/internal/routes/auth.go new file mode 100644 index 0000000..c0cd475 --- /dev/null +++ b/backend/internal/routes/auth.go @@ -0,0 +1,13 @@ +package routes + +import ( + "mini-meeting/internal/handlers" + + "github.com/gofiber/fiber/v2" +) + +func setupAuthRoutes(api fiber.Router, authHandler *handlers.AuthHandler) { + auth := api.Group("/auth") + auth.Get("/:provider", authHandler.OAuthLogin) + auth.Get("/:provider/callback", authHandler.OAuthCallback) +} diff --git a/backend/internal/routes/livekit.go b/backend/internal/routes/livekit.go new file mode 100644 index 0000000..be086a4 --- /dev/null +++ b/backend/internal/routes/livekit.go @@ -0,0 +1,23 @@ +package routes + +import ( + "mini-meeting/internal/config" + "mini-meeting/internal/handlers" + "mini-meeting/internal/middleware" + + "github.com/gofiber/fiber/v2" +) + +func setupLiveKitRoutes(api fiber.Router, livekitHandler *handlers.LiveKitHandler, cfg *config.Config) { + // Public — needed for guests joining a meeting + publicLiveKit := api.Group("/livekit") + publicLiveKit.Post("/token", livekitHandler.GenerateToken) + publicLiveKit.Get("/participants/count", livekitHandler.GetParticipantCount) + + // Protected — host/admin controls + livekit := api.Group("/livekit", middleware.AuthMiddleware(cfg)) + livekit.Get("/participants", livekitHandler.ListParticipants) + livekit.Post("/remove-participant", livekitHandler.RemoveParticipant) + livekit.Post("/mute-participant", livekitHandler.MuteParticipant) + livekit.Post("/end-meeting", livekitHandler.EndMeeting) +} diff --git a/backend/internal/routes/lobby.go b/backend/internal/routes/lobby.go new file mode 100644 index 0000000..bfebb0d --- /dev/null +++ b/backend/internal/routes/lobby.go @@ -0,0 +1,32 @@ +package routes + +import ( + "mini-meeting/internal/handlers" + + "github.com/gofiber/contrib/websocket" + "github.com/gofiber/fiber/v2" +) + +func setupLobbyRoutes( + app *fiber.App, + api fiber.Router, + lobbyHandler *handlers.LobbyHandler, + lobbyWSHandler *handlers.LobbyWSHandler, +) { + // Public lobby routes (guests request / cancel) + publicLobby := api.Group("/lobby") + publicLobby.Post("/request", lobbyHandler.RequestToJoin) + publicLobby.Delete("/request", lobbyHandler.CancelRequest) + + // WebSocket upgrade middleware + app.Use("/ws", func(c *fiber.Ctx) error { + if websocket.IsWebSocketUpgrade(c) { + return c.Next() + } + return fiber.ErrUpgradeRequired + }) + + // WebSocket lobby routes + app.Get("/ws/lobby/visitor", websocket.New(lobbyWSHandler.HandleVisitor)) + app.Get("/ws/lobby/admin", websocket.New(lobbyWSHandler.HandleAdmin)) +} diff --git a/backend/internal/routes/meeting.go b/backend/internal/routes/meeting.go new file mode 100644 index 0000000..aaf86d1 --- /dev/null +++ b/backend/internal/routes/meeting.go @@ -0,0 +1,40 @@ +package routes + +import ( + "mini-meeting/internal/config" + "mini-meeting/internal/handlers" + "mini-meeting/internal/middleware" + + "github.com/gofiber/fiber/v2" +) + +func setupMeetingRoutes( + api fiber.Router, + meetingHandler *handlers.MeetingHandler, + summarizerHandler *handlers.SummarizerHandler, + cfg *config.Config, +) { + // Public — guest accessible + publicMeetings := api.Group("/meetings") + publicMeetings.Get("/code/:code", meetingHandler.GetMeetingByCode) + + // Protected meeting routes + meetings := api.Group("/meetings", middleware.AuthMiddleware(cfg)) + meetings.Post("/", meetingHandler.CreateMeeting) + meetings.Get("/my", meetingHandler.GetMyMeetings) + meetings.Get("/:id", meetingHandler.GetMeeting) + meetings.Delete("/:id", meetingHandler.DeleteMeeting) + + // Summarizer sub-routes (under meetings) + meetings.Post("/:id/summarizer/start", summarizerHandler.StartSummarizer) + meetings.Post("/:id/summarizer/stop", summarizerHandler.StopSummarizer) + + // Admin-only + meetings.Get("/", middleware.AdminMiddleware(), meetingHandler.GetAllMeetings) + + // Session management routes (authenticated) + sessions := api.Group("/sessions", middleware.AuthMiddleware(cfg)) + sessions.Get("/", summarizerHandler.GetSessions) + sessions.Get("/:id", summarizerHandler.GetSession) + sessions.Delete("/:id", summarizerHandler.DeleteSession) +} diff --git a/backend/internal/routes/routes.go b/backend/internal/routes/routes.go index 4c9793a..ec11418 100644 --- a/backend/internal/routes/routes.go +++ b/backend/internal/routes/routes.go @@ -3,83 +3,26 @@ package routes import ( "mini-meeting/internal/config" "mini-meeting/internal/handlers" - "mini-meeting/internal/middleware" - "github.com/gofiber/contrib/websocket" "github.com/gofiber/fiber/v2" ) -func SetupRoutes(app *fiber.App, userHandler *handlers.UserHandler, authHandler *handlers.AuthHandler, meetingHandler *handlers.MeetingHandler, livekitHandler *handlers.LiveKitHandler, lobbyHandler *handlers.LobbyHandler, lobbyWSHandler *handlers.LobbyWSHandler, summarizerHandler *handlers.SummarizerHandler, cfg *config.Config) { - // API routes +func SetupRoutes( + app *fiber.App, + userHandler *handlers.UserHandler, + authHandler *handlers.AuthHandler, + meetingHandler *handlers.MeetingHandler, + livekitHandler *handlers.LiveKitHandler, + lobbyHandler *handlers.LobbyHandler, + lobbyWSHandler *handlers.LobbyWSHandler, + summarizerHandler *handlers.SummarizerHandler, + cfg *config.Config, +) { api := app.Group("/api/v1") - // Auth routes (public) - OAuth only - auth := api.Group("/auth") - auth.Get("/:provider", authHandler.OAuthLogin) - auth.Get("/:provider/callback", authHandler.OAuthCallback) - - // User routes (protected) - users := api.Group("/users", middleware.AuthMiddleware(cfg)) - users.Get("/me", userHandler.GetMe) - users.Patch("/me", userHandler.UpdateMe) - - // Admin-only routes - users.Get("/", middleware.AdminMiddleware(), userHandler.GetAllUsers) - users.Get("/:id", middleware.AdminMiddleware(), userHandler.GetUser) - users.Delete("/:id", middleware.AdminMiddleware(), userHandler.DeleteUser) - - // Public meeting routes (accessible to guests) - publicMeetings := api.Group("/meetings") - publicMeetings.Get("/code/:code", meetingHandler.GetMeetingByCode) - - // Protected meeting routes (require authentication) - meetings := api.Group("/meetings", middleware.AuthMiddleware(cfg)) - meetings.Post("/", meetingHandler.CreateMeeting) - meetings.Get("/my", meetingHandler.GetMyMeetings) - meetings.Get("/:id", meetingHandler.GetMeeting) - meetings.Delete("/:id", meetingHandler.DeleteMeeting) - - // Summarizer routes (protected) - meetings.Post("/:id/summarizer/start", summarizerHandler.StartSummarizer) - meetings.Post("/:id/summarizer/stop", summarizerHandler.StopSummarizer) - - // Session management routes (protected) - sessions := api.Group("/sessions", middleware.AuthMiddleware(cfg)) - sessions.Get("/", summarizerHandler.GetSessions) - sessions.Get("/:id", summarizerHandler.GetSession) - sessions.Delete("/:id", summarizerHandler.DeleteSession) - - // Admin-only meeting routes - meetings.Get("/", middleware.AdminMiddleware(), meetingHandler.GetAllMeetings) - - // Public LiveKit routes (accessible to guests for joining meetings) - publicLiveKit := api.Group("/livekit") - publicLiveKit.Post("/token", livekitHandler.GenerateToken) - publicLiveKit.Get("/participants/count", livekitHandler.GetParticipantCount) - - // Protected LiveKit routes (require authentication) - livekit := api.Group("/livekit", middleware.AuthMiddleware(cfg)) - livekit.Get("/participants", livekitHandler.ListParticipants) - livekit.Post("/remove-participant", livekitHandler.RemoveParticipant) - livekit.Post("/mute-participant", livekitHandler.MuteParticipant) - livekit.Post("/end-meeting", livekitHandler.EndMeeting) - - // Public lobby routes (HTTP — request to join + cancel) - publicLobby := api.Group("/lobby") - publicLobby.Post("/request", lobbyHandler.RequestToJoin) - publicLobby.Delete("/request", lobbyHandler.CancelRequest) - - // Protected lobby routes (admin only — approve/reject via HTTP fallback) - lobby := api.Group("/lobby", middleware.AuthMiddleware(cfg)) - lobby.Post("/respond", lobbyHandler.RespondToRequest) - - // WebSocket lobby routes - app.Use("/ws", func(c *fiber.Ctx) error { - if websocket.IsWebSocketUpgrade(c) { - return c.Next() - } - return fiber.ErrUpgradeRequired - }) - app.Get("/ws/lobby/visitor", websocket.New(lobbyWSHandler.HandleVisitor)) - app.Get("/ws/lobby/admin", websocket.New(lobbyWSHandler.HandleAdmin)) + setupAuthRoutes(api, authHandler) + setupUserRoutes(api, userHandler, cfg) + setupMeetingRoutes(api, meetingHandler, summarizerHandler, cfg) + setupLiveKitRoutes(api, livekitHandler, cfg) + setupLobbyRoutes(app, api, lobbyHandler, lobbyWSHandler) } diff --git a/backend/internal/routes/user.go b/backend/internal/routes/user.go new file mode 100644 index 0000000..d8cfbe0 --- /dev/null +++ b/backend/internal/routes/user.go @@ -0,0 +1,22 @@ +package routes + +import ( + "mini-meeting/internal/config" + "mini-meeting/internal/handlers" + "mini-meeting/internal/middleware" + + "github.com/gofiber/fiber/v2" +) + +func setupUserRoutes(api fiber.Router, userHandler *handlers.UserHandler, cfg *config.Config) { + users := api.Group("/users", middleware.AuthMiddleware(cfg)) + + // Self-service routes + users.Get("/me", userHandler.GetMe) + users.Patch("/me", userHandler.UpdateMe) + + // Admin-only routes + users.Get("/", middleware.AdminMiddleware(), userHandler.GetAllUsers) + users.Get("/:id", middleware.AdminMiddleware(), userHandler.GetUser) + users.Delete("/:id", middleware.AdminMiddleware(), userHandler.DeleteUser) +} diff --git a/frontend/src/services/api/user/index.ts b/frontend/src/services/api/user/index.ts index 8943687..2de7398 100644 --- a/frontend/src/services/api/user/index.ts +++ b/frontend/src/services/api/user/index.ts @@ -11,8 +11,6 @@ export const userService = { updateCurrentUser: userProfileService.updateCurrentUser, getAllUsers: userAdminService.getAllUsers, getUserById: userAdminService.getUserById, - createUser: userAdminService.createUser, - updateUser: userAdminService.updateUser, deleteUser: userAdminService.deleteUser, getSessions: sessionService.getSessions, getSession: sessionService.getSession, diff --git a/frontend/src/services/api/user/user-admin.service.ts b/frontend/src/services/api/user/user-admin.service.ts index 2f42b0d..004d7eb 100644 --- a/frontend/src/services/api/user/user-admin.service.ts +++ b/frontend/src/services/api/user/user-admin.service.ts @@ -3,9 +3,6 @@ import type { User, UserResponse, PaginatedUsersResponse, - UpdateUserRequest, - UserUpdateResponse, - CreateUserRequest, UserDeleteResponse, } from "../../../types/user.types"; @@ -36,25 +33,6 @@ export const userAdminService = { return response.data.data; }, - /** - * Create new user (Admin only) - */ - createUser: async (data: CreateUserRequest): Promise => { - const response = await apiClient.post("/users", data); - return response.data.data; - }, - - /** - * Update user by ID (Admin only) - */ - updateUser: async (id: number, data: UpdateUserRequest): Promise => { - const response = await apiClient.patch( - `/users/${id}`, - data, - ); - return response.data.data; - }, - /** * Delete user (Admin only) */ diff --git a/frontend/src/types/user.types.ts b/frontend/src/types/user.types.ts index a3c0597..d841af6 100644 --- a/frontend/src/types/user.types.ts +++ b/frontend/src/types/user.types.ts @@ -24,27 +24,10 @@ export interface User { avatar_url?: string; created_at: string; } - -export interface UpdateUserRequest { - name?: string; - email?: string; - password?: string; -} - -export interface CreateUserRequest { - email: string; - password: string; - name: string; -} - export interface UserResponse { data: User; } -export interface UsersResponse { - data: User[]; -} - export interface PaginatedUsersResponse { data: User[]; total: number; @@ -53,10 +36,6 @@ export interface PaginatedUsersResponse { total_pages: number; } -export interface UserUpdateResponse { - message: string; - data: User; -} export interface PaginatedSessionsResponse { data: SummarizerSessionList[]; From ebd3ae314c39a5fbc9f15135f022e229b771d937 Mon Sep 17 00:00:00 2001 From: Mohamed Ramadan Date: Fri, 6 Mar 2026 23:56:02 +0200 Subject: [PATCH 4/5] fix: use promise-based user fetch deduplication Replace the boolean fetch lock with a shared fetch promise in userFetcher so concurrent requests return the same promise instead of erroring. This keeps the existing logging and error handling but clears the promise once complete. Update authActions to remove early returns that depended on the old lock, allowing token storage and initialization to proceed even if a user fetch is in progress. --- frontend/src/store/auth/authActions.ts | 7 +---- frontend/src/store/auth/userFetcher.ts | 39 ++++++++++++++------------ 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/frontend/src/store/auth/authActions.ts b/frontend/src/store/auth/authActions.ts index aca337a..36c30a2 100644 --- a/frontend/src/store/auth/authActions.ts +++ b/frontend/src/store/auth/authActions.ts @@ -43,11 +43,6 @@ export const createAuthActions = ( }, setAuthData: async (token: string) => { - if (userFetcher.isLocked()) { - console.warn("⚠️ User fetch already in progress"); - return; - } - try { console.log("🔐 Storing token..."); tokenStorage.set(token); @@ -88,7 +83,7 @@ export const createAuthActions = ( initialize: async () => { const token = get().token; - if (!token || userFetcher.isLocked()) { + if (!token) { set({ isLoading: false }); return; } diff --git a/frontend/src/store/auth/userFetcher.ts b/frontend/src/store/auth/userFetcher.ts index 52e2698..04201f9 100644 --- a/frontend/src/store/auth/userFetcher.ts +++ b/frontend/src/store/auth/userFetcher.ts @@ -1,33 +1,36 @@ import { userService } from "../../services/api/user"; import type { User } from "../../types/user.types"; -let isFetchingUser = false; +let fetchPromise: Promise | null = null; /** * User data fetching with lock mechanism - * Prevents duplicate concurrent requests + * Prevents duplicate concurrent requests by returning the existing promise */ export const userFetcher = { - isLocked: () => isFetchingUser, + isLocked: () => fetchPromise !== null, fetchWithLock: async (): Promise => { - if (isFetchingUser) { - console.warn("⚠️ User fetch already in progress"); - throw new Error("User fetch already in progress"); + if (fetchPromise) { + console.warn("⚠️ User fetch already in progress, returning existing promise"); + return fetchPromise; } - isFetchingUser = true; console.log("🔒 User fetch locked, making API call..."); - try { - const userData = await userService.getCurrentUser(); - console.log("✅ User data received from API:", userData.email); - return userData; - } catch (error) { - console.error("❌ Failed to fetch user from API:", error); - throw error; - } finally { - isFetchingUser = false; - console.log("🔓 User fetch unlocked"); - } + fetchPromise = (async () => { + try { + const userData = await userService.getCurrentUser(); + console.log("✅ User data received from API:", userData.email); + return userData; + } catch (error) { + console.error("❌ Failed to fetch user from API:", error); + throw error; + } finally { + fetchPromise = null; + console.log("🔓 User fetch unlocked"); + } + })(); + + return fetchPromise; }, }; From 5e3b807f0908e27e6b280e965ced4567e4d0a9a3 Mon Sep 17 00:00:00 2001 From: Mohamed Ramadan Date: Sat, 7 Mar 2026 01:16:58 +0200 Subject: [PATCH 5/5] fix: return UpdateUserRequest and UserUpdateResponse --- frontend/src/types/user.types.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/types/user.types.ts b/frontend/src/types/user.types.ts index d841af6..d8a498a 100644 --- a/frontend/src/types/user.types.ts +++ b/frontend/src/types/user.types.ts @@ -48,3 +48,11 @@ export interface PaginatedSessionsResponse { export interface UserDeleteResponse { message: string; } + +export interface UpdateUserRequest { + name?: string; +} + +export interface UserUpdateResponse { + data: User; +}