This repository has been archived by the owner on Apr 30, 2021. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
265 lines (236 loc) · 6.85 KB
/
client.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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
package jexiasdkgo
import (
"fmt"
"log"
"net/http"
"sync"
"time"
)
const (
// DefaultLifetime is 2 hours minus 2 minutes to ensure we never lose the token
DefaultLifetime = 118 * time.Minute
)
// Client contains most data needed for each request
type Client struct {
projectID string
projectZone string
projectURL string
token Token
tokenRequest interface{}
http *http.Client
abortRefresh chan bool
mux sync.Mutex
}
// APKTokenRequest is the JSON data sent to the /auth endpoint when authenticating with the API key
type APKTokenRequest struct {
Method string `json:"method"`
Key string `json:"key"`
Secret string `json:"secret"`
}
// UMSTokenRequest is the JSON data sent to the /auth endpoint when authenticating with user credentials
type UMSTokenRequest struct {
Method string `json:"method"`
Email string `json:"email"`
Password string `json:"password"`
}
// Token is the response from the /auth request which contains the access and refresh tokens
// Currently, each token lasts 2 hours. https://docs.jexia.com/auth/#:~:text=token%20is%20valid%20for
type Token struct {
Access string `json:"access_token"`
Refresh string `json:"refresh_token"`
Lifetime time.Duration
}
// Option allows the client to be configured with different options.
type Option func(*Client)
// SetHTTPClient allows for a custom client to be set
func SetHTTPClient(http *http.Client) Option {
return func(c *Client) {
c.mux.Lock()
c.http = http
c.mux.Unlock()
}
}
// SetProjectURL allows for a custom url to be set which does not match that of the standard pattern
// Note: If such url contains strings such as the project ID, this needs to be computed before being passed through
func SetProjectURL(url string) Option {
return func(c *Client) {
c.mux.Lock()
c.projectURL = url
c.mux.Unlock()
}
}
// SetToken assigns the user token to the client for future use
func (c *Client) SetToken(token Token) {
c.mux.Lock()
c.token = token
c.mux.Unlock()
}
// SetTokenRequest assigns the user token to the client for future use
func (c *Client) SetTokenRequest(tokenRequest interface{}) {
c.mux.Lock()
c.tokenRequest = tokenRequest
c.mux.Unlock()
}
// GetTokenRequest passes the current token request
func (c *Client) GetTokenRequest() interface{} {
c.mux.Lock()
tokenRequest := c.tokenRequest
c.mux.Unlock()
return tokenRequest
}
// GetToken passes the current token
func (c *Client) GetToken() Token {
c.mux.Lock()
token := c.token
c.mux.Unlock()
return token
}
// Token assigns the user token to the client for future use
func (c *Client) fetchToken(target *Token) error {
payload, _ := marshal(c.GetTokenRequest())
err := c.post(
fmt.Sprintf("%v/auth", c.projectURL),
&target,
setBody(payload),
)
if err != nil {
fmt.Printf("error from api. response: %v", err)
return err
}
return nil
}
// ForgetSecrets removes the secret from the APKTokenRequest or the password from the UMSTokenRequest
func (c *Client) ForgetSecrets() {
switch c.GetTokenRequest().(type) {
case APKTokenRequest:
apk := c.GetTokenRequest().(APKTokenRequest)
apk.Secret = ""
c.SetTokenRequest(apk)
case UMSTokenRequest:
ums := c.GetTokenRequest().(UMSTokenRequest)
ums.Password = ""
c.SetTokenRequest(ums)
}
}
// SetTokenLifetime sets the duration before a token refresh is called
// Note: This currently only applies after the first 118 minute loop
// TODO: Ensure that this new duration is set immediately and not after the current loop
func (c *Client) SetTokenLifetime(duration time.Duration) {
// Stop the current lifetime loop
close(c.abortRefresh)
token := c.GetToken()
token.Lifetime = duration
c.SetToken(token)
// Re-open channel
c.abortRefresh = make(chan bool)
}
func (c *Client) setupTokenWithDefaults() error {
var token Token
err := c.fetchToken(&token)
if err != nil {
return err
}
c.SetToken(token)
c.SetTokenLifetime(DefaultLifetime)
return nil
}
// UseAPKToken assigns the user token to the client for future use
func (c *Client) UseAPKToken(apiKey, apiSecret string) error {
c.SetTokenRequest(APKTokenRequest{
Method: "apk",
Key: apiKey,
Secret: apiSecret,
})
err := c.setupTokenWithDefaults()
if err != nil {
return err
}
return nil
}
// UseUMSToken assigns the user token to the client for future use
func (c *Client) UseUMSToken(email, password string) error {
c.SetTokenRequest(UMSTokenRequest{
Method: "ums",
Email: email,
Password: password,
})
err := c.setupTokenWithDefaults()
if err != nil {
return err
}
return nil
}
// RefreshToken triggers a token refresh once called
func (c *Client) RefreshToken() {
var newToken Token
var e Error
token := c.GetToken()
payload, _ := marshal(token)
err := c.post(fmt.Sprintf("%v/auth/refresh", c.projectURL), &newToken, setBody(payload), addToken(token.Access))
// Check what error we get, and if it's temporary
if err != nil {
e = *getNiceError(err, "Error refreshing token")
fmt.Println(e.Error())
}
// If temporary, try again as the connection could have been the issue,
if e.Temporary {
c.RefreshToken()
fmt.Println("Trying to refresh token again")
return
}
if newToken == (Token{}) {
// The request was not successful, we tried to get a token but there was a serious error
log.Fatal("Error refreshing token, there was a unknown, presumed serious error\n")
return
}
// Pass the new tokens over to the existing, ensuring that the lifetime is not changed
token.Access = newToken.Access
token.Refresh = newToken.Refresh
c.SetToken(token)
}
// AutoRefreshToken sets the token to refresh at a certain interval based on token lifetime
func (c *Client) AutoRefreshToken() {
// Assign incase it was stopped and the user want to start it again by calling this function a second time
c.abortRefresh = make(chan bool)
c.newRefreshCycle()
}
// TODO: Ensure that this new duration is set immediately and not after the current loop
func (c *Client) newRefreshCycle() {
go func() {
// start a timer counting down from the token lifetime
lifeLeft := time.NewTimer(c.GetToken().Lifetime)
refreshLoop:
for {
select {
// triggered when the abortRefresh channel is closed
case <-lifeLeft.C:
// refreshes the token and calls another timer
c.RefreshToken()
lifeLeft = time.NewTimer(c.GetToken().Lifetime)
case <-c.abortRefresh:
// exit for loop not switch
break refreshLoop
// triggered when the timer finishes
}
}
}()
}
// NewClient is used to generate a new client for interacting with the API
func NewClient(id, zone string, opts ...Option) *Client {
client := &Client{
projectID: id,
projectZone: zone,
projectURL: fmt.Sprintf("https://%v.%v.app.jexia.com", id, zone),
token: Token{},
tokenRequest: nil,
// TODO: Add optimisations to default http client
http: &http.Client{
Timeout: 15 * time.Second,
},
abortRefresh: make(chan bool),
}
for _, o := range opts {
o(client)
}
return client
}