Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions minutely-api/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,6 @@ func main() {
// Public routes
r.Get("/{meetingId}/transcript", transcriptionHandler.GetMeetingTranscript)
r.Get("/{meetingId}/ai-insights", aiHandler.GetMeetingInsights)
r.Post("/{meetingId}/recordings/upload", transcriptionHandler.UploadRecording)
r.Post("/{meetingId}/transcription/start", liveHandler.StartSession)
r.Post("/{meetingId}/transcription/end", liveHandler.EndSession)

// Authenticated routes
r.Group(func(r chi.Router) {
Expand All @@ -151,6 +148,9 @@ func main() {
r.Get("/", handler.ListMeetings)
r.Get("/summaries", handler.ListMeetingSummaries)
r.Get("/{id}", handler.GetMeeting)
r.Post("/{meetingId}/recordings/upload", transcriptionHandler.UploadRecording)
r.Post("/{meetingId}/transcription/start", liveHandler.StartSession)
r.Post("/{meetingId}/transcription/end", liveHandler.EndSession)
})
})

Expand Down
4 changes: 4 additions & 0 deletions minutely-api/internal/adapters/postgres/dummy_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ func (r *dummyProfileRepo) GetByID(ctx context.Context, id uuid.UUID) (*domain.P
return &domain.Profile{ID: id, FullName: &name}, nil
}

func (r *dummyProfileRepo) Create(ctx context.Context, profile *domain.Profile) error {
return nil
}

func (r *dummyProfileRepo) Update(ctx context.Context, profile *domain.Profile) error {
return nil
}
Expand Down
5 changes: 5 additions & 0 deletions minutely-api/internal/adapters/postgres/supabase_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ func (r *supabaseProfileRepo) GetByID(ctx context.Context, id uuid.UUID) (*domai
return &profile, nil
}

func (r *supabaseProfileRepo) Create(ctx context.Context, profile *domain.Profile) error {
_, _, err := r.client.From("profiles").Insert(profile, false, "exact", "representation", "id").Execute()
return err
}

func (r *supabaseProfileRepo) Update(ctx context.Context, profile *domain.Profile) error {
_, _, err := r.client.From("profiles").Update(profile, "exact", "id").Eq("id", profile.ID.String()).Execute()
return err
Expand Down
1 change: 1 addition & 0 deletions minutely-api/internal/core/domain/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Profile struct {
// ProfileRepository defines datastore interactions for profiles
type ProfileRepository interface {
GetByID(ctx context.Context, id uuid.UUID) (*Profile, error)
Create(ctx context.Context, profile *Profile) error
Update(ctx context.Context, profile *Profile) error
}

Expand Down
14 changes: 13 additions & 1 deletion minutely-api/internal/core/services/profile_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,19 @@ func NewProfileService(repo domain.ProfileRepository) domain.ProfileService {
}

func (s *profileService) GetProfile(ctx context.Context, id uuid.UUID) (*domain.Profile, error) {
return s.repo.GetByID(ctx, id)
profile, err := s.repo.GetByID(ctx, id)
if err != nil {
// If profile not found, create a default one (lazy creation)
// This handles cases where the profiles table was reset but the auth user still exists
newProfile := &domain.Profile{
ID: id,
}
if err := s.repo.Create(ctx, newProfile); err != nil {
Comment on lines +19 to +26
return nil, err
}
return newProfile, nil
}
return profile, nil
}

func (s *profileService) UpdateProfile(ctx context.Context, profile *domain.Profile) error {
Expand Down
5 changes: 5 additions & 0 deletions minutely-api/internal/core/services/transcription_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ func (s *transcriptionService) EndLiveSession(ctx context.Context, meetingID uui
if err == nil {
transcript.Status = domain.TranscriptStatusCompleted
transcript.CompletedAt = &now

// Calculate duration
duration := now.Sub(session.StartedAt).Seconds()
transcript.DurationSecs = &duration

_ = s.repo.Update(ctx, transcript)
}
}
Expand Down
7 changes: 7 additions & 0 deletions minutely-api/internal/transport/http/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ func (h *Handler) GetProfile(w http.ResponseWriter, r *http.Request) {
return
}

// If name is missing in DB, use the one from context (from auth metadata)
if profile.FullName == nil || *profile.FullName == "" || *profile.FullName == "User" {
if name, ok := r.Context().Value(middleware.UserNameKey).(string); ok {
profile.FullName = &name
}
}

json.NewEncoder(w).Encode(profile)
}

Expand Down
11 changes: 11 additions & 0 deletions minutely-api/internal/transport/http/live_transcription_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/google/uuid"

"github.com/MinutelyAI/minutely-api/internal/core/domain"
"github.com/MinutelyAI/minutely-api/internal/transport/http/middleware"
"github.com/MinutelyAI/minutely-api/internal/transport/ws"
)

Expand All @@ -33,10 +34,20 @@ func (h *LiveTranscriptionHandler) parseOrGenerateMeetingID(r *http.Request) uui
if err != nil {
// It's a Jitsi string name, generate a deterministic UUID
meetingID = uuid.NewMD5(uuid.NameSpaceURL, []byte(meetingIDStr))

// Try to extract userID from context (if route is authenticated)
var userIDPtr *uuid.UUID
if val := r.Context().Value(middleware.UserIDKey); val != nil {
if uid, ok := val.(uuid.UUID); ok {
userIDPtr = &uid
}
}

// Lazily create a dummy meeting if it doesn't exist so foreign keys don't fail
_, _ = h.meetingService.GetMeeting(r.Context(), meetingID)
err = h.meetingService.CreateMeeting(r.Context(), &domain.Meeting{
ID: meetingID,
UserID: userIDPtr,
Title: meetingIDStr,
Status: domain.MeetingStatusInProgress,
CreatedAt: time.Now(),
Expand Down
25 changes: 25 additions & 0 deletions minutely-api/internal/transport/http/middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (

type contextKey string
const UserIDKey contextKey = "user_id"
const UserKey contextKey = "user"
const UserNameKey contextKey = "user_name"
const UserEmailKey contextKey = "user_email"

type SupabaseAuth struct {
client *supabase.Client
Expand Down Expand Up @@ -50,6 +53,28 @@ func (m *SupabaseAuth) Handle(next http.Handler) http.Handler {
}

ctx := context.WithValue(r.Context(), UserIDKey, userID)
ctx = context.WithValue(ctx, UserKey, user)

// Extract name from metadata if possible
name := ""
if user.UserMetadata != nil {
if n, ok := user.UserMetadata["full_name"].(string); ok {
name = n
} else if n, ok := user.UserMetadata["name"].(string); ok {
name = n
}
}
if name == "" {
// Fallback to email prefix
if user.Email != "" {
name = strings.Split(user.Email, "@")[0]
} else {
name = "User"
}
}
ctx = context.WithValue(ctx, UserNameKey, name)
ctx = context.WithValue(ctx, UserEmailKey, user.Email)

next.ServeHTTP(w, r.WithContext(ctx))
})
}
21 changes: 20 additions & 1 deletion minutely-api/internal/transport/http/transcription_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,19 @@ func (h *TranscriptionHandler) parseOrGenerateMeetingID(r *http.Request) uuid.UU
meetingID, err := uuid.Parse(meetingIDStr)
if err != nil {
meetingID = uuid.NewMD5(uuid.NameSpaceURL, []byte(meetingIDStr))

// Try to extract userID from context (if route is authenticated)
var userIDPtr *uuid.UUID
if val := r.Context().Value(middleware.UserIDKey); val != nil {
if uid, ok := val.(uuid.UUID); ok {
userIDPtr = &uid
}
}

_, _ = h.meetingService.GetMeeting(r.Context(), meetingID)
err = h.meetingService.CreateMeeting(r.Context(), &domain.Meeting{
ID: meetingID,
UserID: userIDPtr,
Title: meetingIDStr,
Status: domain.MeetingStatusInProgress,
CreatedAt: time.Now(),
Expand All @@ -61,7 +71,11 @@ func (h *TranscriptionHandler) parseOrGenerateMeetingID(r *http.Request) uuid.UU
// UploadRecording accepts a multipart file upload, stores it, and enqueues a transcription job.
// POST /api/v1/meetings/{meetingId}/recordings/upload
func (h *TranscriptionHandler) UploadRecording(w http.ResponseWriter, r *http.Request) {
_ = r.Context().Value(middleware.UserIDKey).(uuid.UUID)
// Safely get UserID
var userID uuid.UUID
if val := r.Context().Value(middleware.UserIDKey); val != nil {
userID = val.(uuid.UUID)
}

meetingID := h.parseOrGenerateMeetingID(r)

Expand Down Expand Up @@ -109,6 +123,11 @@ func (h *TranscriptionHandler) UploadRecording(w http.ResponseWriter, r *http.Re
SizeBytes: &size,
Status: domain.MediaStatusUploaded,
}

if userID != uuid.Nil {
mediaFile.UploadedBy = &userID
}

if err := h.jobRepo.CreateMediaFile(r.Context(), mediaFile); err != nil {
jsonError(w, "failed to record media file: "+err.Error(), http.StatusInternalServerError)
return
Expand Down
10 changes: 9 additions & 1 deletion minutely-api/internal/workers/file_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,16 @@ func (p *FileProcessor) processAIJob(ctx context.Context, job *domain.Processing
}

fullText := ""
if transcript.FullText != nil {
if transcript.FullText != nil && *transcript.FullText != "" {
fullText = *transcript.FullText
} else if len(segments) > 0 {
// Reconstruct from segments if FullText is missing (common for live sessions)
for i, seg := range segments {
if i > 0 {
fullText += " "
}
fullText += seg.Text
}
}

durationSecs := 0.0
Expand Down
Loading
Loading