package auth import ( "context" "encoding/json" "errors" "io" "github.com/pocketbase/pocketbase/tools/types" "golang.org/x/oauth2" ) func init() { Providers[NameBitbucket] = wrapFactory(NewBitbucketProvider) } var _ Provider = (*Bitbucket)(nil) // NameBitbucket is the unique name of the Bitbucket provider. const NameBitbucket = "bitbucket" // Bitbucket is an auth provider for Bitbucket. type Bitbucket struct { BaseProvider } // NewBitbucketProvider creates a new Bitbucket provider instance with some defaults. func NewBitbucketProvider() *Bitbucket { return &Bitbucket{BaseProvider{ ctx: context.Background(), displayName: "Bitbucket", pkce: false, scopes: []string{"account"}, authURL: "https://bitbucket.org/site/oauth2/authorize", tokenURL: "https://bitbucket.org/site/oauth2/access_token", userInfoURL: "https://api.bitbucket.org/2.0/user", }} } // FetchAuthUser returns an AuthUser instance based on the Bitbucket's user API. // // API reference: https://developer.atlassian.com/cloud/bitbucket/rest/api-group-users/#api-user-get func (p *Bitbucket) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { data, err := p.FetchRawUserInfo(token) if err != nil { return nil, err } rawUser := map[string]any{} if err := json.Unmarshal(data, &rawUser); err != nil { return nil, err } extracted := struct { UUID string `json:"uuid"` Username string `json:"username"` DisplayName string `json:"display_name"` AccountStatus string `json:"account_status"` Links struct { Avatar struct { Href string `json:"href"` } `json:"avatar"` } `json:"links"` }{} if err := json.Unmarshal(data, &extracted); err != nil { return nil, err } if extracted.AccountStatus != "active" { return nil, errors.New("the Bitbucket user is not active") } email, err := p.fetchPrimaryEmail(token) if err != nil { return nil, err } user := &AuthUser{ Id: extracted.UUID, Name: extracted.DisplayName, Username: extracted.Username, Email: email, AvatarURL: extracted.Links.Avatar.Href, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, } user.Expiry, _ = types.ParseDateTime(token.Expiry) return user, nil } // fetchPrimaryEmail sends an API request to retrieve the first // verified primary email. // // 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://developer.atlassian.com/cloud/bitbucket/rest/api-group-users/#api-user-emails-get func (p *Bitbucket) fetchPrimaryEmail(token *oauth2.Token) (string, error) { response, err := p.Client(token).Get(p.userInfoURL + "/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 >= 400 { return "", nil } data, err := io.ReadAll(response.Body) if err != nil { return "", err } expected := struct { Values []struct { Email string `json:"email"` IsPrimary bool `json:"is_primary"` } `json:"values"` }{} if err := json.Unmarshal(data, &expected); err != nil { return "", err } for _, v := range expected.Values { if v.IsPrimary { return v.Email, nil } } return "", nil }