etsy: only generate linking urls upon demand
This commit is contained in:
@@ -6,17 +6,18 @@
|
|||||||
- [ ] Etsy (WIP)
|
- [ ] Etsy (WIP)
|
||||||
- [ ] GET ETSY AUTH (WIP)
|
- [ ] GET ETSY AUTH (WIP)
|
||||||
- [x] move auth state stuff to database (out of cache)
|
- [x] move auth state stuff to database (out of cache)
|
||||||
- [ ] only generate a sign up link IF they click the link on the accounts page
|
- [x] only generate a sign up link IF they click the link on the accounts page
|
||||||
- [ ] get api key approved
|
- [ ] get api key approved
|
||||||
- [o] NEEDS TESTING - Get new access token using refresh token flow
|
- [?] Get new access token using refresh token flow
|
||||||
- [ ] make a FK between the etsy_store_events table and etsy_users table (store_id columns don't match types)
|
- [ ] make a FK between the etsy_store_events table and etsy_users table (store_id columns don't match types)
|
||||||
- [ ] Auth0
|
- [ ] Auth0
|
||||||
- [ ] get off dev api key?
|
- [ ] get off dev api key?
|
||||||
- [ ] Get new access token using refresh token flow
|
- [x] Get new access token using refresh token flow
|
||||||
- [ ] test
|
|
||||||
- [ ] Get new refresh token flow
|
|
||||||
- [ ] test
|
- [ ] test
|
||||||
- [ ] Social connections login
|
- [ ] Social connections login
|
||||||
|
- [ ] automatically clean up access tokens and state when expired
|
||||||
|
- [ ] access tokens
|
||||||
|
- [ ] state
|
||||||
- [ ] Complete this design document?
|
- [ ] Complete this design document?
|
||||||
- [ ] Complete defining this roadmap checklist
|
- [ ] Complete defining this roadmap checklist
|
||||||
- [ ] Website displaying an audit of store events
|
- [ ] Website displaying an audit of store events
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
{{- else }}
|
{{- else }}
|
||||||
<h3>
|
<h3>
|
||||||
{{/* TODO: create this link dynamically, not EVERYTIME THE PAGE IS LOADED */}}
|
{{/* TODO: create this link dynamically, not EVERYTIME THE PAGE IS LOADED */}}
|
||||||
<a href="{{ .Etsy.GenerateConnectionURLForNewAccount $acctID.ID }}">
|
<a href="{{ printf "/webhooks/etsy/%d/new-account-link" $acctID.ID }}">
|
||||||
Link Your Etsy Store!
|
Link Your Etsy Store!
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -11,99 +11,139 @@ import (
|
|||||||
"ruben/inventory2/internal/domains/raw_events"
|
"ruben/inventory2/internal/domains/raw_events"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type (
|
||||||
OAuthRedirectURIWithAcctIDParam string
|
Webhooks struct {
|
||||||
}
|
cfg Config
|
||||||
|
db *raw_events.Store
|
||||||
|
etsy *etsy.Platform
|
||||||
|
}
|
||||||
|
|
||||||
|
Config struct {
|
||||||
|
OAuthRedirectURIWithAcctIDParam string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func NewWebhookHandler(
|
func NewWebhookHandler(
|
||||||
db *raw_events.Store,
|
db *raw_events.Store,
|
||||||
platform *etsy.Platform,
|
platform *etsy.Platform,
|
||||||
cfg Config,
|
cfg Config,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
|
h := Webhooks{
|
||||||
|
cfg: cfg,
|
||||||
|
db: db,
|
||||||
|
etsy: platform,
|
||||||
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.HandleFunc("POST /test", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("POST /test", h.test)
|
||||||
var body json.RawMessage
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
||||||
http.Error(w, "Failed to decode body as json: "+err.Error(), 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ts := time.Now().UTC()
|
mux.HandleFunc("GET "+h.cfg.OAuthRedirectURIWithAcctIDParam, h.redirectURI)
|
||||||
|
|
||||||
storeID := "test-store-id"
|
mux.HandleFunc("GET /{acctID}/new-account-link", h.newAccountLink)
|
||||||
var payloadObject struct {
|
|
||||||
StoreID string
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(body, &payloadObject); err == nil && payloadObject.StoreID != "" {
|
|
||||||
storeID = payloadObject.StoreID
|
|
||||||
}
|
|
||||||
|
|
||||||
err := db.Save(r.Context(), &raw_events.Event{
|
|
||||||
Platform: "etsy",
|
|
||||||
StoreID: storeID,
|
|
||||||
EventID: fmt.Sprint(ts.Unix()),
|
|
||||||
EventTimestamp: ts,
|
|
||||||
Payload: body,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Error occurred saving the body as the event payload: "+err.Error(), 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(201)
|
|
||||||
})
|
|
||||||
|
|
||||||
mux.HandleFunc("GET "+cfg.OAuthRedirectURIWithAcctIDParam, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// get account id for the request
|
|
||||||
|
|
||||||
acctID, err := strconv.ParseInt(r.PathValue("acctID"), 10, 64)
|
|
||||||
if err != nil || acctID <= 0 {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
q := r.URL.Query()
|
|
||||||
state := q.Get("state")
|
|
||||||
|
|
||||||
// handle failed, potentially non-consenting, request
|
|
||||||
|
|
||||||
if errCode := q.Get("error"); errCode != "" {
|
|
||||||
errDesc := q.Get("error_description")
|
|
||||||
errURI := q.Get("error_uri")
|
|
||||||
|
|
||||||
fmt.Printf(
|
|
||||||
"error in obtaining an OAuth Token: error=%s, error_desc=%s, error_uri=%s, account_id=%d\n",
|
|
||||||
errCode,
|
|
||||||
errDesc,
|
|
||||||
errURI,
|
|
||||||
acctID,
|
|
||||||
)
|
|
||||||
|
|
||||||
platform.InvalidateState(ctx, state)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate the state to prevent CSRF attacks
|
|
||||||
|
|
||||||
ok, err := platform.HandleNewAuthCode(ctx, acctID, state, q.Get("code"))
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
|
||||||
fmt.Println("failed to handle new auth code:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: response with a redirect to the user's account page (SUCCESS - new sign up or login)!
|
|
||||||
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("/accounts/%d", acctID), http.StatusSeeOther)
|
|
||||||
})
|
|
||||||
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /test
|
||||||
|
func (h Webhooks) test(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var body json.RawMessage
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
http.Error(w, "Failed to decode body as json: "+err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := time.Now().UTC()
|
||||||
|
|
||||||
|
storeID := "test-store-id"
|
||||||
|
var payloadObject struct {
|
||||||
|
StoreID string
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &payloadObject); err == nil && payloadObject.StoreID != "" {
|
||||||
|
storeID = payloadObject.StoreID
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.db.Save(r.Context(), &raw_events.Event{
|
||||||
|
Platform: "etsy",
|
||||||
|
StoreID: storeID,
|
||||||
|
EventID: fmt.Sprint(ts.Unix()),
|
||||||
|
EventTimestamp: ts,
|
||||||
|
Payload: body,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error occurred saving the body as the event payload: "+err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(201)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET h.cfg.OAuthRedirectURIWithAcctIDParam
|
||||||
|
func (h Webhooks) redirectURI(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// get account id for the request
|
||||||
|
|
||||||
|
acctID, err := strconv.ParseInt(r.PathValue("acctID"), 10, 64)
|
||||||
|
if err != nil || acctID <= 0 {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
q := r.URL.Query()
|
||||||
|
state := q.Get("state")
|
||||||
|
|
||||||
|
// handle failed, potentially non-consenting, request
|
||||||
|
|
||||||
|
if errCode := q.Get("error"); errCode != "" {
|
||||||
|
errDesc := q.Get("error_description")
|
||||||
|
errURI := q.Get("error_uri")
|
||||||
|
|
||||||
|
fmt.Printf(
|
||||||
|
"error in obtaining an OAuth Token: error=%s, error_desc=%s, error_uri=%s, account_id=%d\n",
|
||||||
|
errCode,
|
||||||
|
errDesc,
|
||||||
|
errURI,
|
||||||
|
acctID,
|
||||||
|
)
|
||||||
|
|
||||||
|
h.etsy.InvalidateState(ctx, state)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate the state to prevent CSRF attacks
|
||||||
|
|
||||||
|
ok, err := h.etsy.HandleNewAuthCode(ctx, acctID, state, q.Get("code"))
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
fmt.Println("failed to handle new auth code:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect to the user's account page
|
||||||
|
|
||||||
|
http.Redirect(w, r, fmt.Sprintf("/accounts/%d", acctID), http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /{acctID}/new-account-link
|
||||||
|
func (h Webhooks) newAccountLink(w http.ResponseWriter, r *http.Request) {
|
||||||
|
acctIDStr := r.PathValue("acctID")
|
||||||
|
acctID, err := strconv.ParseInt(acctIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("account %s not found", acctIDStr), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := h.etsy.GenerateConnectionURLForNewAccount(r.Context(), acctID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to generate url for account %d: %v", acctID, err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user