etsy: only generate linking urls upon demand

This commit is contained in:
2026-01-05 15:40:35 -07:00
parent 5b3a4b5468
commit 55a7fcfede
3 changed files with 130 additions and 89 deletions
+6 -5
View File
@@ -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>
+123 -83
View File
@@ -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)
}