270 lines
5.2 KiB
Go
270 lines
5.2 KiB
Go
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 *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)
|
|
}
|