diff --git a/tools/auth/gitee.go b/tools/auth/gitee.go index af90dc5d..c66f7643 100644 --- a/tools/auth/gitee.go +++ b/tools/auth/gitee.go @@ -5,6 +5,7 @@ import ( "io" "strconv" + "github.com/go-ozzo/ozzo-validation/v4/is" "golang.org/x/oauth2" ) @@ -57,51 +58,73 @@ func (p *Gitee) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id: strconv.Itoa(extracted.Id), Name: extracted.Name, Username: extracted.Login, - Email: extracted.Email, AvatarUrl: extracted.AvatarUrl, RawUser: rawUser, AccessToken: token.AccessToken, } - // in case user set "Keep my email address private", - // email should be retrieved via extra API request - if user.Email == "" { - client := p.Client(token) - - response, err := client.Get("https://gitee.com/api/v5/emails") + if extracted.Email != "" && is.EmailFormat.Validate(extracted.Email) == nil { + // valid public primary email + user.Email = extracted.Email + } else { + // send an additional optional request to retrieve the email + email, err := p.fetchPrimaryEmail(token) if err != nil { - return user, err - } - defer response.Body.Close() - - content, err := io.ReadAll(response.Body) - if err != nil { - return user, err - } - - emails := []struct { - Email string - State string - Scope []string - }{} - if err := json.Unmarshal(content, &emails); err != nil { - return user, err - } - - // extract the verified primary email - - // - // API reference: https://gitee.com/api/v5/swagger#/getV5Emails - outer: - for _, email := range emails { - for _, scope := range email.Scope { - if email.State == "confirmed" && scope == "primary" { - user.Email = email.Email - break outer - } - } + return nil, err } + user.Email = email } return user, nil } + +// fetchPrimaryEmail sends an API request to retrieve the verified primary email, +// in case the user hasn't set "Public email address" or has unchecked +// the "Access your emails data" permission during authentication. +// +// NB! This method can succeed and still return an empty email. +// Error responses that are result of insufficient scopes permissions are ignored. +// +// API reference: https://gitee.com/api/v5/swagger#/getV5Emails +func (p *Gitee) fetchPrimaryEmail(token *oauth2.Token) (string, error) { + client := p.Client(token) + + response, err := client.Get("https://gitee.com/api/v5/emails") + if err != nil { + return "", err + } + defer response.Body.Close() + + // ignore common http errors caused by insufficient scope permissions + if response.StatusCode == 401 || response.StatusCode == 403 || response.StatusCode == 404 { + return "", nil + } + + content, err := io.ReadAll(response.Body) + if err != nil { + return "", err + } + + emails := []struct { + Email string + State string + Scope []string + }{} + if err := json.Unmarshal(content, &emails); err != nil { + // ignore unmarshal error in case "Keep my email address private" + // was set because response.Body will be something like: + // {"email":"12285415+test@user.noreply.gitee.com"} + return "", nil + } + + // extract the first verified primary email + for _, email := range emails { + for _, scope := range email.Scope { + if email.State == "confirmed" && scope == "primary" && is.EmailFormat.Validate(email.Email) == nil { + return email.Email, nil + } + } + } + + return "", nil +} diff --git a/tools/auth/github.go b/tools/auth/github.go index b7de2714..7af01d06 100644 --- a/tools/auth/github.go +++ b/tools/auth/github.go @@ -67,42 +67,58 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { // in case user has set "Keep my email address private", send an // **optional** API request to retrieve the verified primary email if user.Email == "" { - client := p.Client(token) - - response, err := client.Get(p.userApiUrl + "/emails") + email, err := p.fetchPrimaryEmail(token) if err != nil { - return user, err - } - defer response.Body.Close() - - // ignore not found errors caused by unsufficient scope permissions - // (the email field is optional, return the auth user without it) - if response.StatusCode == 404 { - return user, nil - } - - content, err := io.ReadAll(response.Body) - if err != nil { - return user, err - } - - emails := []struct { - Email string - Verified bool - Primary bool - }{} - if err := json.Unmarshal(content, &emails); err != nil { - return user, err - } - - // extract the verified primary email - for _, email := range emails { - if email.Verified && email.Primary { - user.Email = email.Email - break - } + return nil, err } + user.Email = email } return user, nil } + +// fetchPrimaryEmail sends an API request to retrieve the verified +// primary email, in case "Keep my email address private" was set. +// +// NB! This method can succeed and still return an empty email. +// Error responses that are result of insufficient scopes permissions are ignored. +// +// API reference: https://docs.github.com/en/rest/users/emails?apiVersion=2022-11-28 +func (p *Github) fetchPrimaryEmail(token *oauth2.Token) (string, error) { + client := p.Client(token) + + response, err := client.Get(p.userApiUrl + "/emails") + if err != nil { + return "", err + } + defer response.Body.Close() + + // ignore common http errors caused by insufficient scope permissions + // (the email field is optional, aka. return the auth user without it) + if response.StatusCode == 401 || response.StatusCode == 403 || response.StatusCode == 404 { + return "", nil + } + + content, err := io.ReadAll(response.Body) + if err != nil { + return "", err + } + + emails := []struct { + Email string + Verified bool + Primary bool + }{} + if err := json.Unmarshal(content, &emails); err != nil { + return "", err + } + + // extract the verified primary email + for _, email := range emails { + if email.Verified && email.Primary { + return email.Email, nil + } + } + + return "", nil +}