-
-
Save Bo0mer/99cfd8e7d6270db4a4ae4758b5c56193 to your computer and use it in GitHub Desktop.
Yellowfin Upload Avatar
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"context" | |
"encoding/base64" | |
"encoding/json" | |
"fmt" | |
"io" | |
"io/ioutil" | |
"log" | |
"mime/multipart" | |
"net/http" | |
"os" | |
"strconv" | |
"strings" | |
"sync" | |
"time" | |
"github.com/google/uuid" | |
) | |
func main() { | |
var ( | |
addr = os.Getenv("YF_ADDRESS") | |
username = os.Getenv("YF_USERNAME") | |
password = os.Getenv("YF_PASSWORD") | |
filename = os.Getenv("AVATAR_FILE") | |
userIDString = os.Getenv("YF_AVATAR_CHANGE_USER_ID") | |
) | |
ctx := context.Background() | |
yf, err := NewClient(addr, username, password) | |
if err != nil { | |
log.Fatalf("error initializing yellowfin client: %v", err) | |
} | |
imageBytes, err := ioutil.ReadFile(filename) | |
if err != nil { | |
log.Fatalf("error reading file contents: %v", err) | |
} | |
userID, err := strconv.Atoi(userIDString) | |
if err != nil { | |
log.Fatalf("invalid user id provided: %v", err) | |
} | |
err = yf.UpdateUserAvatar(ctx, userID, filename, imageBytes) | |
if err != nil { | |
log.Fatalf("error updating avatar: %s", err) | |
} | |
} | |
const defaultOrg = "" | |
type token struct { | |
raw string | |
exp time.Time | |
} | |
func parseToken(t string) (*token, error) { | |
parts := strings.Split(t, ".") | |
if len(parts) != 3 { | |
return nil, fmt.Errorf("invalid number of parts: %d", len(parts)) | |
} | |
body := parts[1] | |
bodyString, err := base64.RawStdEncoding.DecodeString(body) | |
if err != nil { | |
return nil, fmt.Errorf("invalid token body: %w", err) | |
} | |
// Format is as follows. | |
// {"role":"YFADMIN","ti":109298,"person":"5","client":"1","exp":1647519351,"iat":1647518151} | |
var claims struct { | |
Role string `json:"role"` | |
TI int64 `json:"ti"` | |
Person string `json:"person"` | |
Client string `json:"client"` | |
Expiry int64 `json:"exp"` | |
IssuedAt int64 `json:"iat"` | |
} | |
err = json.Unmarshal([]byte(bodyString), &claims) | |
if err != nil { | |
return nil, fmt.Errorf("invalid token body: %w", err) | |
} | |
return &token{ | |
raw: t, | |
exp: time.Unix(claims.Expiry, 0).UTC(), | |
}, nil | |
} | |
func (t *token) Valid() bool { | |
return t != nil && t.exp.After(time.Now().UTC().Add(-5*time.Minute)) | |
} | |
func (t *token) Raw() string { | |
return t.raw | |
} | |
type Client struct { | |
httpClient *http.Client | |
addr string | |
username string | |
password string | |
refreshToken string | |
mu sync.Mutex // guards | |
accessToken *token | |
} | |
func NewClient(addr, username, password string) (*Client, error) { | |
c := &Client{ | |
httpClient: http.DefaultClient, | |
addr: addr, | |
username: username, | |
password: password, | |
} | |
var ( | |
ctx = context.Background() | |
err error | |
) | |
c.refreshToken, err = c.issueRefreshToken(ctx, defaultOrg) | |
if err != nil { | |
return nil, fmt.Errorf("error authenticating client: %w", err) | |
} | |
return c, nil | |
} | |
func (c *Client) UpdateUserAvatar(ctx context.Context, userID int, filename string, file []byte) error { | |
body := new(bytes.Buffer) | |
writer := multipart.NewWriter(body) | |
formWriter, err := writer.CreateFormFile("avatar", filename) | |
if err != nil { | |
return err | |
} | |
formWriter.Write(file) | |
writer.Close() | |
req, err := c.newMultipartRequestWithToken(ctx, "POST", fmt.Sprintf("/api/users/%d/avatar", userID), body, writer.FormDataContentType()) | |
if err != nil { | |
return err | |
} | |
resp, err := c.do(req) | |
if err != nil { | |
return err | |
} | |
defer resp.Body.Close() | |
return nil | |
} | |
func (c *Client) issueRefreshToken(ctx context.Context, org string) (string, error) { | |
body := new(bytes.Buffer) | |
err := json.NewEncoder(body).Encode(struct { | |
Username string `json:"userName"` | |
Password string `json:"password"` | |
Organization string `json:"clientOrgRef"` | |
}{c.username, c.password, org}) | |
if err != nil { | |
return "", fmt.Errorf("error encoding request body: %w", err) | |
} | |
req, err := http.NewRequestWithContext(ctx, "POST", c.addr+"/api/refresh-tokens", body) | |
if err != nil { | |
return "", err | |
} | |
req.Header.Set("Accept", "application/vnd.yellowfin.api-v2+json;") | |
req.Header.Set("Content-Type", "application/json") | |
req.Header.Set("Authorization", | |
fmt.Sprintf("YELLOWFIN ts=%d, nonce=%s", | |
time.Now().UnixMilli(), | |
uuid.Must(uuid.NewRandom()).String(), | |
)) | |
resp, err := c.httpClient.Do(req) | |
if err != nil { | |
return "", fmt.Errorf("error performing request: %w", err) | |
} | |
defer resp.Body.Close() | |
if resp.StatusCode != http.StatusOK { | |
return "", fmt.Errorf("unexpected response code: %d", resp.StatusCode) | |
} | |
d := struct { | |
SecurityToken string `json:"securityToken"` | |
}{} | |
err = json.NewDecoder(resp.Body).Decode(&d) | |
if err != nil { | |
return "", err | |
} | |
return d.SecurityToken, nil | |
} | |
func (c *Client) token(ctx context.Context) (string, error) { | |
c.mu.Lock() | |
defer c.mu.Unlock() | |
if c.accessToken.Valid() { | |
return c.accessToken.Raw(), nil | |
} | |
var err error | |
c.accessToken, err = c.issueAccessToken(ctx, c.refreshToken) | |
if err != nil { | |
return "", fmt.Errorf("error issuing access token: %w", err) | |
} | |
return c.accessToken.Raw(), nil | |
} | |
func (c *Client) issueAccessToken(ctx context.Context, refreshToken string) (*token, error) { | |
req, err := c.newRequestWithToken(ctx, refreshToken, "POST", "/api/access-tokens", nil) | |
if err != nil { | |
return nil, err | |
} | |
resp, err := c.do(req) | |
if err != nil { | |
return nil, err | |
} | |
defer resp.Body.Close() | |
if resp.StatusCode != http.StatusOK { | |
return nil, fmt.Errorf("unexpected http status code: %d", resp.StatusCode) | |
} | |
d := struct { | |
SecurityToken string `json:"securityToken"` | |
}{} | |
err = json.NewDecoder(resp.Body).Decode(&d) | |
if err != nil { | |
return nil, fmt.Errorf("error parsing response: %w", err) | |
} | |
return parseToken(d.SecurityToken) | |
} | |
func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { | |
token, err := c.token(ctx) | |
if err != nil { | |
return nil, err | |
} | |
return c.newRequestWithToken(ctx, token, method, path, body) | |
} | |
func (c *Client) newRequestWithToken(ctx context.Context, t, method, path string, body io.Reader) (*http.Request, error) { | |
req, err := http.NewRequestWithContext(ctx, method, c.addr+path, body) | |
if err != nil { | |
return nil, fmt.Errorf("error creating request: %w", err) | |
} | |
req.Header.Set("Accept", "application/vnd.yellowfin.api-v2+json;") | |
req.Header.Set("Content-Type", "application/json") | |
req.Header.Set("Authorization", | |
fmt.Sprintf("YELLOWFIN ts=%d, nonce=%s, token=%s", | |
time.Now().UnixMilli(), | |
uuid.Must(uuid.NewRandom()).String(), | |
t, | |
), | |
) | |
return req, nil | |
} | |
func (c *Client) newMultipartRequestWithToken(ctx context.Context, method, path string, body io.Reader, contentType string) (*http.Request, error) { | |
token, err := c.token(ctx) | |
if err != nil { | |
return nil, err | |
} | |
req, err := http.NewRequestWithContext(ctx, method, c.addr+path, body) | |
if err != nil { | |
return nil, fmt.Errorf("error creating request: %w", err) | |
} | |
req.Header.Set("Accept", "application/vnd.yellowfin.api-v2+json;") | |
req.Header.Set("Content-Type", contentType) | |
req.Header.Set("Authorization", | |
fmt.Sprintf("YELLOWFIN ts=%d, nonce=%s, token=%s", | |
time.Now().UnixMilli(), | |
uuid.Must(uuid.NewRandom()).String(), | |
token, | |
), | |
) | |
return req, nil | |
} | |
func (c *Client) do(req *http.Request) (*http.Response, error) { | |
resp, err := c.httpClient.Do(req) | |
if err != nil { | |
return nil, fmt.Errorf("error performing request: %w", err) | |
} | |
if resp.StatusCode != http.StatusOK { | |
defer resp.Body.Close() | |
r := io.LimitReader(resp.Body, 1024*1024*1024) | |
body, err := io.ReadAll(r) | |
if err == nil { | |
return nil, fmt.Errorf("unexpected http status code %d with body:\n%s", resp.StatusCode, body) | |
} | |
return nil, fmt.Errorf("unexpected http status code: %d", resp.StatusCode) | |
} | |
return resp, nil | |
} | |
func (c *Client) doReq(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { | |
req, err := c.newRequest(ctx, method, path, body) | |
if err != nil { | |
return nil, err | |
} | |
resp, err := c.do(req) | |
if err != nil { | |
return nil, err | |
} | |
return resp, nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment