Files

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)
}