forked from pocketbase/pocketbase
/
bitbucket.go
132 lines (112 loc) · 3.31 KB
/
bitbucket.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package auth
import (
"context"
"encoding/json"
"errors"
"io"
"github.com/pocketbase/pocketbase/tools/types"
"golang.org/x/oauth2"
)
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",
userApiUrl: "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.FetchRawUserData(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.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 >= 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
}