From 08a92ba2d0eb5deb4917b4c7414f7f4f3b14d9d7 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 2 Feb 2026 21:02:32 +0800 Subject: [PATCH] feat: Add multiple context and editor hooks to ChatInput component. --- api/api.go | 44 +++++++++++++++++++++++++++------------ internal/auth/oauth.go | 47 ++++++++++++++++++++++++++++++++---------- 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/api/api.go b/api/api.go index ed399d4..ae697e7 100644 --- a/api/api.go +++ b/api/api.go @@ -2344,9 +2344,14 @@ func (h *handler) oauthCallback(c *gin.Context) { return } - // 1. Check if user exists by identity + if !profile.Verified { + respondError(c, http.StatusUnauthorized, "email_not_verified", "oauth email must be verified") + return + } + + var user *store.User ctx := c.Request.Context() - user, err := h.store.GetUserByEmail(ctx, profile.Email) + existingUser, err := h.store.GetUserByEmail(ctx, profile.Email) if err != nil && !errors.Is(err, store.ErrUserNotFound) { respondError(c, http.StatusInternalServerError, "store_error", "database error") return @@ -2357,26 +2362,17 @@ func (h *handler) oauthCallback(c *gin.Context) { user = &store.User{ Name: profile.Name, Email: profile.Email, - EmailVerified: true, // Trusted provider + EmailVerified: true, // Trusted provider, verified above Level: store.LevelUser, Role: store.RoleUser, Groups: []string{"User"}, + Active: true, } if err := h.store.CreateUser(ctx, user); err != nil { respondError(c, http.StatusInternalServerError, "user_creation_failed", "failed to create user") return } - // Track identity - identity := &store.Identity{ - UserID: user.ID, - Provider: providerName, - ExternalID: profile.ID, - } - if err := h.store.CreateIdentity(ctx, identity); err != nil { - slog.Warn("failed to create identity record", "err", err, "userID", user.ID) - } - // Provision trial trialExpiresAt := time.Now().UTC().Add(7 * 24 * time.Hour) trial := &store.Subscription{ @@ -2390,6 +2386,28 @@ func (h *handler) oauthCallback(c *gin.Context) { Meta: map[string]any{"expiresAt": trialExpiresAt}, } h.store.UpsertSubscription(ctx, trial) + } else { + user = existingUser + // Ensure user is verified if they logged in via OAuth + if !user.EmailVerified { + user.EmailVerified = true + if err := h.store.UpdateUser(ctx, user); err != nil { + slog.Warn("failed to update user verification status during oauth", "err", err, "userID", user.ID) + } + } + } + + // Always ensure identity record exists (bind OAuth ID to User Email) + identity := &store.Identity{ + UserID: user.ID, + Provider: providerName, + ExternalID: profile.ID, + } + if err := h.store.CreateIdentity(ctx, identity); err != nil { + // Only log error if it's not a "already exists" error + if !strings.Contains(err.Error(), "exists") { + slog.Warn("failed to create identity record during oauth binding", "err", err, "userID", user.ID) + } } // Create session or generate public token for frontend redirect diff --git a/internal/auth/oauth.go b/internal/auth/oauth.go index 8a5b554..0c8fc24 100644 --- a/internal/auth/oauth.go +++ b/internal/auth/oauth.go @@ -12,9 +12,10 @@ import ( // OAuthUserProfile represents the unified user profile info from OAuth providers. type OAuthUserProfile struct { - ID string `json:"id"` - Email string `json:"email"` - Name string `json:"name"` + ID string `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + Verified bool `json:"verified"` } // OAuthProvider defines the interface for different OAuth2 providers. @@ -95,13 +96,35 @@ func (p *GitHubProvider) FetchProfile(ctx context.Context, token *oauth2.Token) if err == nil { defer resp.Body.Close() var emails []struct { - Email string `json:"email"` - Primary bool `json:"primary"` + Email string `json:"email"` + Primary bool `json:"primary"` + Verified bool `json:"verified"` } if err := json.NewDecoder(resp.Body).Decode(&emails); err == nil { for _, e := range emails { if e.Primary { profile.Email = e.Email + profile.Verified = e.Verified + break + } + } + } + } + } else { + // If we got email from /user, we still want to know if it's verified. + // GitHub /user doesn't return verification status, usually it's better + // to always fetch from /user/emails for accuracy if verification is required. + resp, err := client.Get("https://api.github.com/user/emails") + if err == nil { + defer resp.Body.Close() + var emails []struct { + Email string `json:"email"` + Verified bool `json:"verified"` + } + if err := json.NewDecoder(resp.Body).Decode(&emails); err == nil { + for _, e := range emails { + if e.Email == profile.Email { + profile.Verified = e.Verified break } } @@ -144,17 +167,19 @@ func (p *GoogleProvider) FetchProfile(ctx context.Context, token *oauth2.Token) defer resp.Body.Close() var user struct { - ID string `json:"id"` - Email string `json:"email"` - Name string `json:"name"` + ID string `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + VerifiedEmail bool `json:"verified_email"` } if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { return nil, err } return &OAuthUserProfile{ - ID: user.ID, - Email: user.Email, - Name: user.Name, + ID: user.ID, + Email: user.Email, + Name: user.Name, + Verified: user.VerifiedEmail, }, nil }