account page stubbed: link to etsy sign up
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package etsy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"ruben/inventory2/internal/domains/platforms/etsy/generated_client"
|
||||
)
|
||||
|
||||
func newFixedAccessTokenClient(apiKey, accessToken string) (*generated_client.ClientWithResponses, error) {
|
||||
return newAuthenticatingClient(apiKey, func(ctx context.Context) (string, error) {
|
||||
return accessToken, nil
|
||||
})
|
||||
}
|
||||
|
||||
func newAccessTokenRefreshingClient(apiKey string) (*generated_client.ClientWithResponses, error) {
|
||||
return newAuthenticatingClient(apiKey, func(ctx context.Context) (string, error) {
|
||||
// TODO: look up the token in the db.
|
||||
return "", nil
|
||||
})
|
||||
}
|
||||
|
||||
func newAuthenticatingClient(apiKey string, getAccessToken func(context.Context) (string, error)) (*generated_client.ClientWithResponses, error) {
|
||||
var setAuthHeaders generated_client.ClientOption = func(c *generated_client.Client) error {
|
||||
c.RequestEditors = append(c.RequestEditors, generated_client.RequestEditorFn(func(ctx context.Context, req *http.Request) error {
|
||||
accessToken, err := getAccessToken(req.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h := req.Header
|
||||
h.Set("x-api-key", apiKey)
|
||||
h.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||
|
||||
return nil
|
||||
}))
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := generated_client.NewClientWithResponses("https://api.etsy.com", setAuthHeaders)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize open api client: %w", err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
@@ -1,19 +1,320 @@
|
||||
package etsy
|
||||
|
||||
//go:generate oapi-codegen -package generated_client -o generated_client/client.go openapi.3.0.2.json
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
const (
|
||||
etsyAPIKeystring = "38ncokqh0jih5jshfk8iv4n5"
|
||||
etsyAPISharedSecret = "jaaw0tyizf"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"ruben/inventory2/internal/domains/platforms/etsy/generated_client"
|
||||
)
|
||||
|
||||
func GetEtsyAPIKeystring() string {
|
||||
return etsyAPIKeystring
|
||||
//go:generate oapi-codegen -generate types,client -package generated_client -o generated_client/client.go openapi.3.0.2.json
|
||||
|
||||
// TODO:
|
||||
// - [x] document the flow in the README.md
|
||||
// - [ ] save the initial access/refresh tokens in the db
|
||||
// - [ ] make calls to refresh the access token and store it in the db.
|
||||
// - [ ] make cron jobs to automatically refresh the refresh token upon as tokens approach expiration.
|
||||
// - [ ] make pages to direct to the etsy shop acceptance url/page
|
||||
// - [ ] stub an account page
|
||||
|
||||
type (
|
||||
Platform struct {
|
||||
oAuthRedirectURI func(acctID int64) string
|
||||
apiKeystring string
|
||||
apiSharedSecret string
|
||||
db *pgxpool.Pool
|
||||
}
|
||||
|
||||
PlatformWithContext struct {
|
||||
ctx context.Context
|
||||
p *Platform
|
||||
}
|
||||
)
|
||||
|
||||
// oauth scopes
|
||||
const (
|
||||
scopeAddressRead = "address_r" // Read a member's shipping addresses.
|
||||
scopeAddressWrite = "address_w" // Update and delete a member's shipping address.
|
||||
scopeBillingRead = "billing_r" // Read a member's Etsy bill charges and payments.
|
||||
scopeCartRead = "cart_r" // Read the contents of a member’s cart.
|
||||
scopeCartWrite = "cart_w" // Add and remove listings from a member's cart.
|
||||
scopeEmailRead = "email_r" // Read a user profile
|
||||
scopeFavoritesRead = "favorites_r" // View a member's favorite listings and users.
|
||||
scopeFavoritesWrite = "favorites_w" // Add to and remove from a member's favorite listings and users.
|
||||
scopeFeedbackRead = "feedback_r" // View all details of a member's feedback (including purchase history.)
|
||||
scopeListings_d = "listings_d" // Delete a member's listings.
|
||||
scopeListingsRead = "listings_r" // Read a member's inactive and expired (i.e., non-public) listings.
|
||||
scopeListingsWrite = "listings_w" // Create and edit a member's listings.
|
||||
scopeProfileRead = "profile_r" // Read a member's private profile information.
|
||||
scopeProfileWrite = "profile_w" // Update a member's private profile information.
|
||||
scopeRecommendRead = "recommend_r" // View a member's recommended listings.
|
||||
scopeRecommendWrite = "recommend_w" // Remove a member's recommended listings.
|
||||
scopeShopsRead = "shops_r" // See a member's shop description, messages and sections, even if not (yet) public.
|
||||
scopeShopsWrite = "shops_w" // Update a member's shop description, messages and sections.
|
||||
scopeTransactionsRead = "transactions_r" // Read a member's purchase and sales data. This applies to buyers as well as sellers.
|
||||
scopeTransactionsWrite = "transactions_w" // Update a member's sales data.
|
||||
)
|
||||
|
||||
func NewPlatform(oAuthRedirectURI func(acctID int64) string, apiKeystring, apiSharedSecret string, db *pgxpool.Pool) *Platform {
|
||||
return &Platform{
|
||||
oAuthRedirectURI: oAuthRedirectURI,
|
||||
apiKeystring: apiKeystring,
|
||||
apiSharedSecret: apiSharedSecret,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func GetEtsyAPISharedSecret() string {
|
||||
return etsyAPISharedSecret
|
||||
func (p *Platform) WithContext(ctx context.Context) *PlatformWithContext {
|
||||
return &PlatformWithContext{
|
||||
ctx: ctx,
|
||||
p: p,
|
||||
}
|
||||
}
|
||||
|
||||
func requestAnAuthCode() {
|
||||
func (p *Platform) GenerateConnectionURLForNewAccount(ctx context.Context, acctID int64) (*url.URL, error) {
|
||||
req, err := p.createNewOAuthRequest(ctx, acctID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create oauth request parameters: %w", err)
|
||||
}
|
||||
|
||||
state := req.state
|
||||
code := req.pkceCode
|
||||
|
||||
return &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "www.etsy.com",
|
||||
Path: "/oauth/connect",
|
||||
RawQuery: url.Values{
|
||||
"response_type": {"code"},
|
||||
"redirect_uri": {p.oAuthRedirectURI(acctID)},
|
||||
"scope": {url.QueryEscape(strings.Join([]string{
|
||||
scopeCartRead,
|
||||
scopeCartWrite,
|
||||
scopeEmailRead,
|
||||
scopeListingsWrite,
|
||||
}, ","))},
|
||||
"client_id": {p.apiKeystring},
|
||||
"state": {state.String()},
|
||||
"code_challenge": {fmt.Sprintf("%x", code.challenge)},
|
||||
"code_challenge_method": {"S256"},
|
||||
}.Encode(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *PlatformWithContext) GenerateConnectionURLForNewAccount(acctID int64) (*url.URL, error) {
|
||||
return p.p.GenerateConnectionURLForNewAccount(p.ctx, acctID)
|
||||
}
|
||||
|
||||
// HandleNewAuthCode handles the auth code to get api access
|
||||
func (p *Platform) HandleNewAuthCode(ctx context.Context, acctID int64, state, authCode string) (bool, error) {
|
||||
// look up matching oauth request
|
||||
|
||||
stateUUID, err := uuid.Parse(state)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
oar, ok, err := p.getOauthRequest(ctx, stateUUID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to look up existing oauth request: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
if oar.acctID != acctID {
|
||||
return false, p.deleteOauthRequest(ctx, stateUUID)
|
||||
}
|
||||
|
||||
// construct http request to obtain access token
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
(&url.URL{
|
||||
Scheme: "https://",
|
||||
Host: "api.etsy.com",
|
||||
Path: "/v3/public/oauth/token",
|
||||
}).String(),
|
||||
bytes.NewReader([]byte(url.Values{
|
||||
"grant_type": {"authorization_code"},
|
||||
"client_id": {p.apiKeystring},
|
||||
"redirect_uri": {p.oAuthRedirectURI(acctID)},
|
||||
"code": {authCode},
|
||||
"code_verifier": {fmt.Sprintf("%x", oar.pkceCode.verifier)},
|
||||
}.Encode())),
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to generate http request to get oauth tokens: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "x-www-form-urlencoded")
|
||||
|
||||
// perform request to obtain access token
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to perform http request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return false, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
if c := resp.StatusCode; (c / 100) != 2 {
|
||||
return false, fmt.Errorf("unexpected http response code: %d; body = %s", c, string(b))
|
||||
}
|
||||
|
||||
// parse and validate response body
|
||||
|
||||
accessToken, refreshToken, expiration, userID, err := p.parseAccessCodeResponseBody(ctx, b)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to parse response body: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("new access token:", accessToken)
|
||||
fmt.Println("new refresh token:", refreshToken)
|
||||
fmt.Println("new token expiration:", expiration)
|
||||
|
||||
// look up user's shop id
|
||||
|
||||
shopID, err := p.getNewUserShopID(ctx, accessToken, userID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to complete sign on due to failing to look up the user's shop id: %w", err)
|
||||
}
|
||||
|
||||
// save all to database
|
||||
const ninetyDays = 90 * 24 * time.Hour
|
||||
|
||||
if err := p.saveNewEtsyUser(
|
||||
ctx,
|
||||
EtsyUser{
|
||||
AcctID: acctID,
|
||||
UserID: userID,
|
||||
ShopID: shopID,
|
||||
},
|
||||
etsyAccessTokens{
|
||||
access: tokenAndExpiration{
|
||||
token: accessToken,
|
||||
expiration: expiration,
|
||||
},
|
||||
refresh: tokenAndExpiration{
|
||||
token: refreshToken,
|
||||
expiration: time.Now().Add(ninetyDays),
|
||||
},
|
||||
},
|
||||
); err != nil {
|
||||
return false, fmt.Errorf("failed to save new user and access token: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (p *Platform) parseAccessCodeResponseBody(ctx context.Context, body []byte) (
|
||||
accessToken string,
|
||||
refreshToken string,
|
||||
expiration time.Time,
|
||||
userID int64, // stored in the access token & refresh token
|
||||
err error,
|
||||
) {
|
||||
// parse and validate response body
|
||||
|
||||
var (
|
||||
tokenType string
|
||||
expirationInSeconds int
|
||||
)
|
||||
|
||||
if err = json.Unmarshal(body, &struct {
|
||||
Access_Token *string
|
||||
Token_Type *string
|
||||
Expires_In *int
|
||||
Refresh_Token *string
|
||||
}{
|
||||
Access_Token: &accessToken,
|
||||
Token_Type: &tokenType,
|
||||
Expires_In: &expirationInSeconds,
|
||||
Refresh_Token: &refreshToken,
|
||||
}); err != nil {
|
||||
err = fmt.Errorf("failed to decode request body as json: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
if v := accessToken; v == "" {
|
||||
err = fmt.Errorf("no access token specified in access token response: body = %s", string(body))
|
||||
return
|
||||
} else if accessTokenParts := strings.SplitN(accessToken, ".", 2); len(accessTokenParts) < 2 {
|
||||
err = fmt.Errorf("unexpected access token format: expected a user_id prefix: <user_id>.<remaindeder>: %s: body = %s", accessToken, string(body))
|
||||
return
|
||||
} else if i, ierr := strconv.ParseInt(accessTokenParts[0], 10, 64); ierr != nil {
|
||||
err = fmt.Errorf("unexpected user_id in access token: should be an integer: %s: %w", accessTokenParts[0], ierr)
|
||||
return
|
||||
} else if i <= 0 {
|
||||
err = fmt.Errorf("unexpected user_id in access token: should be a positive integer: %d: %w", i, err)
|
||||
} else {
|
||||
userID = i
|
||||
}
|
||||
|
||||
if tt := tokenType; tt == "" {
|
||||
err = fmt.Errorf("no token type specified in access token response: body = %s", string(body))
|
||||
return
|
||||
} else if tt != "Bearer" {
|
||||
err = fmt.Errorf("unexpected token type specified in access token response: %s: body = %s", tt, string(body))
|
||||
return
|
||||
}
|
||||
|
||||
if v := expirationInSeconds; v == 0 {
|
||||
err = fmt.Errorf("no expiration specified in access token response: body = %s", string(body))
|
||||
return
|
||||
} else if v < 0 {
|
||||
err = fmt.Errorf("unexpected expiration specified in access token response: %d: body = %s", v, string(body))
|
||||
return
|
||||
} else {
|
||||
expiration = time.Now().UTC().Add(time.Duration(expirationInSeconds) * time.Second)
|
||||
}
|
||||
|
||||
if v := refreshToken; v == "" {
|
||||
err = fmt.Errorf("no refresh token specified in access token response: body = %s", string(body))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Platform) getNewUserShopID(ctx context.Context, accessToken string, userID int64) (int64, error) {
|
||||
cli, err := newFixedAccessTokenClient(p.apiKeystring, accessToken)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to initialize openapi client: %w", err)
|
||||
}
|
||||
|
||||
var res *generated_client.GetShopByOwnerUserIdResponse
|
||||
if res, err = cli.GetShopByOwnerUserIdWithResponse(ctx, userID); err != nil {
|
||||
return 0, fmt.Errorf("failed to obtain shop id due to failure to initialize request to obtain shop id: %w", err)
|
||||
} else if res.JSON400 != nil {
|
||||
return 0, fmt.Errorf("failed to look up shop for user %d due to 400 error: %s", userID, res.JSON400.Error)
|
||||
} else if res.JSON403 != nil {
|
||||
return 0, fmt.Errorf("failed to look up shop for user %d due to 403 error: %s", userID, res.JSON403.Error)
|
||||
} else if res.JSON404 != nil {
|
||||
return 0, fmt.Errorf("failed to look up shop for user %d due to 404 error: %s", userID, res.JSON404.Error)
|
||||
} else if res.JSON500 != nil {
|
||||
return 0, fmt.Errorf("failed to look up shop for user %d due to 500 error: %s", userID, res.JSON500.Error)
|
||||
}
|
||||
|
||||
if res.JSON200.ShopId == nil {
|
||||
return 0, fmt.Errorf("shop for user %d has no shop_id set: body = %s", userID, res.Body)
|
||||
}
|
||||
|
||||
return *res.JSON200.ShopId, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
package etsy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type (
|
||||
EtsyUser struct {
|
||||
AcctID int64
|
||||
UserID int64
|
||||
ShopID int64
|
||||
}
|
||||
|
||||
etsyAccessTokens struct {
|
||||
access tokenAndExpiration
|
||||
refresh tokenAndExpiration
|
||||
}
|
||||
|
||||
tokenAndExpiration struct {
|
||||
token string
|
||||
expiration time.Time
|
||||
}
|
||||
|
||||
oauth2Request struct {
|
||||
acctID int64
|
||||
state uuid.UUID
|
||||
expiration time.Time
|
||||
pkceCode pkceCode
|
||||
}
|
||||
|
||||
pkceCode struct {
|
||||
verifier [32]byte
|
||||
challenge []byte
|
||||
}
|
||||
)
|
||||
|
||||
func (p *Platform) GetUserPointerByAccountID(ctx context.Context, acctID int64) (*EtsyUser, error) {
|
||||
rows, err := p.db.Query(
|
||||
ctx,
|
||||
"SELECT user_id, shop_id FROM etsy_users WHERE account_id = @account_id",
|
||||
pgx.NamedArgs{
|
||||
"account_id": acctID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to perform query: %w", err)
|
||||
}
|
||||
|
||||
type Row struct {
|
||||
User_ID int64
|
||||
Shop_ID int64
|
||||
}
|
||||
|
||||
r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to scan row: %w", err)
|
||||
}
|
||||
|
||||
return &EtsyUser{
|
||||
AcctID: acctID,
|
||||
UserID: r.User_ID,
|
||||
ShopID: r.Shop_ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *PlatformWithContext) GetUserPointerByAccountID(acctID int64) (*EtsyUser, error) {
|
||||
return p.p.GetUserPointerByAccountID(p.ctx, acctID)
|
||||
}
|
||||
|
||||
func (p *Platform) saveNewEtsyUser(
|
||||
ctx context.Context,
|
||||
user EtsyUser,
|
||||
tokens etsyAccessTokens,
|
||||
) (err error) {
|
||||
_, err = p.db.Exec(
|
||||
ctx,
|
||||
`
|
||||
WITH new_user (
|
||||
INSERT INTO etsy_users (
|
||||
account_id,
|
||||
user_id,
|
||||
shop_id
|
||||
)
|
||||
VALUES (
|
||||
@account_id,
|
||||
@user_id,
|
||||
@shop_id
|
||||
)
|
||||
RETURNING
|
||||
account_id,
|
||||
user_id,
|
||||
shop_id
|
||||
)
|
||||
INSERT INTO etsy_access_tokens (
|
||||
access_token,
|
||||
refresh_token,
|
||||
access_token_expiration,
|
||||
refresh_token_expiration
|
||||
)
|
||||
VALUE (
|
||||
@access_token,
|
||||
@refresh_token,
|
||||
@access_token_expiration,
|
||||
@refresh_token_expiration
|
||||
)
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"account_id": user.AcctID,
|
||||
"user_id": user.UserID,
|
||||
"shop_id": user.ShopID,
|
||||
"access_token": tokens.access.token,
|
||||
"refresh_token": tokens.refresh.token,
|
||||
"access_token_expiration": tokens.access.expiration,
|
||||
"refresh_token_expiration": tokens.refresh.expiration,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert new records: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: clean these up on timer.
|
||||
func (p *Platform) createNewOAuthRequest(ctx context.Context, acctID int64) (oauth2Request, error) {
|
||||
req := oauth2Request{
|
||||
acctID: acctID,
|
||||
state: uuid.New(),
|
||||
expiration: time.Now().UTC().Add(10 * time.Minute),
|
||||
pkceCode: newPKCECode(),
|
||||
}
|
||||
|
||||
_, err := p.db.Exec(
|
||||
ctx,
|
||||
`
|
||||
INSERT INTO etsy_oauth_requests (
|
||||
account_id,
|
||||
state,
|
||||
code_verifier,
|
||||
expiration
|
||||
)
|
||||
VALUES (
|
||||
@account_id,
|
||||
@state,
|
||||
@code_verifier,
|
||||
@expiration
|
||||
)
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"account_id": req.acctID,
|
||||
"state": req.state[:],
|
||||
"code_verifier": req.pkceCode.verifier[:],
|
||||
"expiration": req.expiration,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return oauth2Request{}, fmt.Errorf("failed to insert record: %w", err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (p *Platform) getOauthRequest(ctx context.Context, state uuid.UUID) (req oauth2Request, ok bool, err error) {
|
||||
stateBytes := [16]byte(state)
|
||||
|
||||
rows, err := p.db.Query(
|
||||
ctx,
|
||||
`
|
||||
SELECT
|
||||
account_id,
|
||||
code_verifier,
|
||||
expiration
|
||||
FROM
|
||||
etsy_oauth_requests
|
||||
WHERE
|
||||
state = @state
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"state": stateBytes[:],
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return oauth2Request{}, false, fmt.Errorf("failed to perform query: %w", err)
|
||||
}
|
||||
|
||||
type Row struct {
|
||||
Account_ID int64
|
||||
Code_Verifier []byte
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return oauth2Request{}, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if l := len(r.Code_Verifier); l != 32 {
|
||||
return oauth2Request{}, false, fmt.Errorf("code_verifier of unexpected length found: expected = 32, found = %d", l)
|
||||
}
|
||||
|
||||
var verifier [32]byte
|
||||
copy(verifier[:], r.Code_Verifier)
|
||||
|
||||
return oauth2Request{
|
||||
acctID: r.Account_ID,
|
||||
state: state,
|
||||
expiration: r.Expiration.UTC(),
|
||||
pkceCode: pkceCode{
|
||||
verifier: verifier,
|
||||
challenge: generateCodeChallenge(verifier),
|
||||
},
|
||||
}, false, nil
|
||||
}
|
||||
|
||||
func (p *Platform) InvalidateState(ctx context.Context, state string) error {
|
||||
stateUUID, err := uuid.Parse(state)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.deleteOauthRequest(ctx, stateUUID)
|
||||
}
|
||||
|
||||
func (p *Platform) deleteOauthRequest(ctx context.Context, state uuid.UUID) error {
|
||||
_, err := p.db.Exec(
|
||||
ctx,
|
||||
`
|
||||
DELETE FROM etsy_oauth_requests
|
||||
WHERE state = @state
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"state": state,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute query: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newPKCECode() pkceCode {
|
||||
var code pkceCode
|
||||
part1 := [16]byte(uuid.New())
|
||||
part2 := [16]byte(uuid.New())
|
||||
copy(code.verifier[0:16], part1[:])
|
||||
copy(code.verifier[16:32], part2[:])
|
||||
|
||||
code.challenge = generateCodeChallenge(code.verifier)
|
||||
return code
|
||||
}
|
||||
|
||||
func generateCodeChallenge(codeVerifier [32]byte) []byte {
|
||||
return generateSHA256Hash(codeVerifier[:])
|
||||
}
|
||||
|
||||
func generateSHA256Hash(b []byte) []byte {
|
||||
h := sha256.New()
|
||||
h.Write(b)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
Reference in New Issue
Block a user