package auth import ( "context" "encoding/json" "fmt" "io" "net/http" "github.com/Sosokker/todolist-backend/internal/config" "golang.org/x/oauth2" googleOAuth "golang.org/x/oauth2/google" ) // GoogleUserInfo holds user details fetched from Google. type GoogleUserInfo struct { ID string `json:"sub"` Email string `json:"email"` VerifiedEmail bool `json:"email_verified"` Name string `json:"name"` Picture string `json:"picture"` } // OAuthProvider defines the interface for OAuth operations. type OAuthProvider interface { GetAuthCodeURL(state string) string ExchangeCode(ctx context.Context, code string) (*oauth2.Token, error) FetchUserInfo(ctx context.Context, token *oauth2.Token) (*GoogleUserInfo, error) GetOAuth2Config() *oauth2.Config // Expose underlying config if needed } // googleOAuthProvider implements OAuthProvider for Google. type googleOAuthProvider struct { cfg *oauth2.Config } // NewGoogleOAuthProvider creates a new provider instance configured for Google. func NewGoogleOAuthProvider(appCfg *config.Config) OAuthProvider { return &googleOAuthProvider{ cfg: &oauth2.Config{ ClientID: appCfg.OAuth.Google.ClientID, ClientSecret: appCfg.OAuth.Google.ClientSecret, RedirectURL: appCfg.OAuth.Google.RedirectURL, Scopes: appCfg.OAuth.Google.Scopes, Endpoint: googleOAuth.Endpoint, }, } } // GetAuthCodeURL generates the URL for Google's consent page. func (g *googleOAuthProvider) GetAuthCodeURL(state string) string { // Add options like AccessTypeOffline to get a refresh token, authURL := g.cfg.AuthCodeURL(state, oauth2.AccessTypeOffline /*, oauth2.ApprovalForce, oauth2.SetAuthURLParam("prompt", "select_account") */) return authURL } // ExchangeCode exchanges the authorization code for an access token and refresh token. func (g *googleOAuthProvider) ExchangeCode(ctx context.Context, code string) (*oauth2.Token, error) { token, err := g.cfg.Exchange(ctx, code) if err != nil { return nil, fmt.Errorf("failed to exchange google auth code '%s': %w", code, err) } if !token.Valid() { return nil, fmt.Errorf("exchanged token is invalid") } return token, nil } // FetchUserInfo uses the access token to get user details from Google's UserInfo endpoint. func (g *googleOAuthProvider) FetchUserInfo(ctx context.Context, token *oauth2.Token) (*GoogleUserInfo, error) { client := g.cfg.Client(ctx, token) userInfoURL := "https://www.googleapis.com/oauth2/v3/userinfo" // v3 is common resp, err := client.Get(userInfoURL) if err != nil { return nil, fmt.Errorf("failed to request google user info from %s: %w", userInfoURL, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("google user info request failed with status %d: %s", resp.StatusCode, string(bodyBytes)) } var userInfo GoogleUserInfo if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { return nil, fmt.Errorf("failed to decode google user info response: %w", err) } if userInfo.ID == "" || userInfo.Email == "" { return nil, fmt.Errorf("invalid user info received from google (missing ID or Email)") } return &userInfo, nil } // GetOAuth2Config returns the underlying oauth2.Config object. func (g *googleOAuthProvider) GetOAuth2Config() *oauth2.Config { return g.cfg }