diff --git a/README.md b/README.md index fa6b460..d5d338f 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,18 @@ - [ ] Etsy (WIP) - [ ] GET ETSY AUTH (WIP) - [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 - - [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) - [ ] Auth0 - [ ] get off dev api key? - - [ ] Get new access token using refresh token flow - - [ ] test - - [ ] Get new refresh token flow + - [x] Get new access token using refresh token flow - [ ] test - [ ] Social connections login + - [ ] automatically clean up access tokens and state when expired + - [ ] access tokens + - [ ] state - [ ] Complete this design document? - [ ] Complete defining this roadmap checklist - [ ] Website displaying an audit of store events diff --git a/internal/site/templates/page_bodies/accounts/{acctID}/index.html.tmpl b/internal/site/templates/page_bodies/accounts/{acctID}/index.html.tmpl index 2314644..8d48c6c 100644 --- a/internal/site/templates/page_bodies/accounts/{acctID}/index.html.tmpl +++ b/internal/site/templates/page_bodies/accounts/{acctID}/index.html.tmpl @@ -12,7 +12,7 @@ {{- else }}

{{/* TODO: create this link dynamically, not EVERYTIME THE PAGE IS LOADED */}} - + Link Your Etsy Store!

diff --git a/internal/webhooks/etsy/webhooks.go b/internal/webhooks/etsy/webhooks.go index 38f1303..b35cfdb 100644 --- a/internal/webhooks/etsy/webhooks.go +++ b/internal/webhooks/etsy/webhooks.go @@ -11,99 +11,139 @@ import ( "ruben/inventory2/internal/domains/raw_events" ) -type Config struct { - OAuthRedirectURIWithAcctIDParam string -} +type ( + Webhooks struct { + cfg Config + db *raw_events.Store + etsy *etsy.Platform + } + + Config struct { + OAuthRedirectURIWithAcctIDParam string + } +) func NewWebhookHandler( db *raw_events.Store, platform *etsy.Platform, cfg Config, ) http.Handler { + h := Webhooks{ + cfg: cfg, + db: db, + etsy: platform, + } + mux := http.NewServeMux() - mux.HandleFunc("POST /test", func(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 - } + mux.HandleFunc("POST /test", h.test) - ts := time.Now().UTC() + mux.HandleFunc("GET "+h.cfg.OAuthRedirectURIWithAcctIDParam, h.redirectURI) - storeID := "test-store-id" - 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) - }) + mux.HandleFunc("GET /{acctID}/new-account-link", h.newAccountLink) 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) +}