removed intermediate /internal directory

This commit is contained in:
2026-02-09 13:40:31 -07:00
parent b155280081
commit 0944703d2a
50 changed files with 117 additions and 101 deletions
+491
View File
@@ -0,0 +1,491 @@
package accounts
import (
"errors"
"fmt"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"ruben/inventory2/consts"
"ruben/inventory2/domains/accounts"
"ruben/inventory2/logging"
"ruben/inventory2/server/auth"
"ruben/inventory2/server/param"
"ruben/inventory2/server/response"
"ruben/inventory2/server/sse"
)
type accountSubrouter struct {
log *logging.Logger
accts *accounts.Store
pub *sse.UpdateNotificationPublisher
}
func Routes(
r *gin.RouterGroup,
logger *logging.Logger,
accts *accounts.Store,
pub *sse.UpdateNotificationPublisher,
) {
as := &accountSubrouter{
log: logger,
accts: accts,
pub: pub,
}
r.POST("", response.Handler(as.createAccount))
platformGroup := r.Group("/:acctID/platforms/:platform", pub.Publish("/:acctID/platforms"))
platformGroup.PUT("/order-index", response.Handler(as.setOrderOfPlatformOnAccountPage))
platformGroup.POST("/shops/mocks", response.Handler(as.createMockShop))
mockShops := platformGroup.Group("/shops/mocks/:shop-id")
mockShops.POST("/listings", response.Handler(as.addMockListing))
mockShops.PUT("/listings/:listing-id", response.Handler(as.updateMockListing))
mockShops.DELETE("/listings/:listing-id", response.Handler(as.deleteMockListing))
syncGroups := r.Group("/:acctID/inventory/sync-groups")
syncGroups.POST("", pub.Publish("/:acctID/inventory/sync-groups"), response.Handler(as.saveNewSyncGroup))
draftListings := syncGroups.Group("/draft/listings", pub.Publish("/:acctID/inventory/sync-groups/draft/listings"))
draftListings.POST("", response.Handler(as.createSyncGroupListingDraft))
draftListings.PUT("/:orderIndex/shop", response.Handler(as.setShopInSyncGroupListingDraft))
draftListings.PUT("/:orderIndex/listing", response.Handler(as.setListingInSyncGroupListingDraft))
draftListings.DELETE("/:orderIndex", response.Handler(as.deleteSyncGroupListingDraft))
mockSyncGroups := syncGroups.Group("/mock")
mockDraftListings := mockSyncGroups.Group("/draft/listings", pub.Publish("/:acctID/inventory/sync-groups/mock/draft/listings"))
mockDraftListings.POST("", response.Handler(as.createMockSyncGroupListingDraft))
mockDraftListings.PUT("/:orderIndex/shop", response.Handler(as.setShopInMockSyncGroupListingDraft))
mockDraftListings.PUT("/:orderIndex/listing", response.Handler(as.setListingInMockSyncGroupListingDraft))
mockDraftListings.DELETE("/:orderIndex", response.Handler(as.deleteMockSyncGroupListingDraft))
}
// POST /
func (s *accountSubrouter) createAccount(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
// TODO: get email from the user's account profile (how?) - will need to flesh out the account creation process later on
email := uuid.NewString() + "@example.com"
userID := auth.GetIdentity(ctx).User.UserID
_, err := s.accts.CreateAccount(ctx, userID, email)
if err != nil {
if errors.Is(err, consts.ErrConflict) {
return nil, response.Conflict().
Msg("user already has an account")
}
return nil, response.Errorf("failed to create account: %w", err)
}
return response.StatusCreated().
// force a reload to ensure an sse connection is made.
// user should automatically be redirected to the appropriate page based on their current location and/or login/account statuses.
HXRefresh("true"), nil
}
// POST /:acctID/inventory/sync-groups/draft/listings
func (s *accountSubrouter) createSyncGroupListingDraft(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := auth.GetIdentity(ctx).Account.AccountID
if _, err := s.accts.CreateSyncGroupListingDraft(ctx, acctID); err != nil {
return nil, response.Errorf("failed to create new listing draft: %w", err)
}
return response.StatusCreated(), nil
}
// PUT /:acctID/inventory/sync-groups/draft/listings/:orderIndex/shop
// @platform string
// @shopID string
func (s *accountSubrouter) setShopInSyncGroupListingDraft(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := auth.GetIdentity(ctx).Account.AccountID
var (
orderIndex int
platform accounts.Platform
shopID string
)
err := param.Path("orderIndex", param.Int(&orderIndex)).
Form("platform", param.Platform(&platform)).
Form("shop-id", param.Text(&shopID)).
Unmarshal(c)
if err != nil {
return nil, err
}
if err := s.accts.SetShopInSyncGroupListingDraft(ctx, acctID, orderIndex, platform, shopID); err != nil {
return nil, response.Errorf("failed to set shop: %w", response.ErrorFromConstant(err))
}
return response.StatusOK(), nil
}
// PUT /:acctID/inventory/sync-groups/draft/listings/:orderIndex/listing
func (s *accountSubrouter) setListingInSyncGroupListingDraft(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := auth.GetIdentity(ctx).Account.AccountID
var (
orderIndex int
listingID string
)
err := param.Path("orderIndex", param.Int(&orderIndex)).
Form("listing-id", param.Text(&listingID)).
Unmarshal(c)
if err != nil {
return nil, err
}
if err := s.accts.SetListingInSyncGroupListingDraft(ctx, acctID, orderIndex, listingID); err != nil {
return nil, response.Errorf("failed to set listing: %w", response.ErrorFromConstant(err))
}
return response.StatusOK(), nil
}
// DELETE /:acctID/inventory/sync-groups/draft/listings/:orderIndex
func (s *accountSubrouter) deleteSyncGroupListingDraft(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := auth.GetIdentity(ctx).Account.AccountID
var (
orderIndex int
)
err := param.Path("orderIndex", param.Int(&orderIndex)).
Unmarshal(c)
if err != nil {
return nil, err
}
if _, err := s.accts.DeleteSyncGroupListingDraft(ctx, acctID, orderIndex); err != nil {
return nil, response.Errorf("failed to delete listing: %w", response.ErrorFromConstant(err))
}
return response.StatusOK(), nil
}
// POST /:acctID/inventory/sync-groups
func (s *accountSubrouter) saveNewSyncGroup(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := auth.GetIdentity(ctx).Account.AccountID
if _, err := s.accts.SaveNewSyncGroup(ctx, acctID); err != nil {
return nil, response.Errorf("failed to save new sync group: %w", response.ErrorFromConstant(err))
}
return response.StatusCreated(), nil
}
// PUT /:acctID/platforms/:platform/order-index"
// this endpoint is called when dragging a platform tab in the accounts page.
func (s *accountSubrouter) setOrderOfPlatformOnAccountPage(c *gin.Context) (response.Response, error) {
acctID := auth.GetIdentity(c).Account.AccountID
// validate parameters
var (
platform accounts.Platform
orderIndex int
)
err := param.Path("platform", param.Platform(&platform)).
Form("order-index", param.Int(&orderIndex)).
Unmarshal(c)
if err != nil {
return nil, err
}
// update the order
platforms, prevIndex, err := s.accts.SetOrderOfPlatformOnAccountPage(c, acctID, platform, orderIndex)
if err != nil {
return nil, fmt.Errorf("failed to save record: %w", err)
}
// emit events on all platforms updated, so the tabs can all refresh (including their logic)
platformEvents := make([]string, max(orderIndex, prevIndex)-min(orderIndex, prevIndex))
if orderIndex > prevIndex {
for i := range orderIndex - prevIndex {
p := platforms[prevIndex+i]
platformEvents[i] = fmt.Sprintf("accounts_%d_platforms_%s_order-index", acctID, lowerSnakeCase(p))
}
} else if orderIndex < prevIndex {
for i := range prevIndex - orderIndex {
p := platforms[orderIndex+1+i]
platformEvents[i] = fmt.Sprintf("accounts_%d_platforms_%s_order-index", acctID, lowerSnakeCase(p))
}
}
if err := s.pub.Push(c, acctID, platformEvents...); err != nil {
s.log.Errorf("failed to publish platform order-index events: %v", err)
}
return response.StatusNoContent(), nil
}
// POST /:acctID/platforms/:platform/shops/mocks
func (s *accountSubrouter) createMockShop(c *gin.Context) (response.Response, error) {
acctID := auth.GetIdentity(c).Account.AccountID
// validate parameters
var (
platform accounts.Platform
name string
)
err := param.Path("platform", param.Platform(&platform)).
Form("name", param.Text(&name)).
Unmarshal(c)
if err != nil {
return nil, err
}
s.log.Warnf("not implemented: mock store created: account_id = %d; platform = %s; name = %s", acctID, platform, name)
// create the mock account
id, err := s.accts.CreateMockShop(c, acctID, platform, name)
if err != nil {
return nil, fmt.Errorf("failed to save record: %w", err)
}
location := fmt.Sprintf("/ui/accounts/%d/platforms/%s/mock-shops/%s", acctID, lowerSnakeCase(platform), id)
var res response.Response
if c.GetHeader("HX-Request") == "true" {
res = response.StatusCreated()
} else {
res = response.SeeOther(location)
}
return res.HXLocation(location), nil
}
// POST /:acctID/platforms/:platform/shops/mocks/:shop-id/listings
func (s *accountSubrouter) addMockListing(c *gin.Context) (response.Response, error) {
acctID := auth.GetIdentity(c).Account.AccountID
var (
platform accounts.Platform
shopID string
name string
sku string
description string
count int
)
err := param.Path("platform", param.Platform(&platform)).
Path("shop-id", param.Text(&shopID)).
Form("name", param.Text(&name)).
Form("sku", param.Text(&sku)).
Form("description", param.Text(&description)).
Form("count", param.Int(&count)).
Unmarshal(c)
if err != nil {
return nil, err
}
_, err = s.accts.CreateMockListing(
c,
accounts.MockListing{
AccountShopListingIDs: accounts.NewAccountIDs(acctID).
ShopID(platform, shopID).
ListingID(""),
Name: name,
SKU: sku,
Description: description,
Count: int64(count),
},
)
if err != nil {
return nil, fmt.Errorf("failed to create new listing: %w", err)
}
return response.StatusCreated(), nil
}
// PUT /:acctID/platforms/:platform/shops/mocks/:shop-id/listings/:listing-id
func (s *accountSubrouter) updateMockListing(c *gin.Context) (response.Response, error) {
acctID := auth.GetIdentity(c).Account.AccountID
var (
platform accounts.Platform
shopID string
listingID string
name string
sku string
description string
count int
)
err := param.Path("platform", param.Platform(&platform)).
Path("shop-id", param.Text(&shopID)).
Path("listing-id", param.Text(&listingID)).
Form("name", param.Text(&name)).
Form("sku", param.Text(&sku)).
Form("description", param.Text(&description)).
Form("count", param.Int(&count)).
Unmarshal(c)
if err != nil {
return nil, err
}
err = s.accts.UpdateMockListing(
c,
accounts.MockListing{
AccountShopListingIDs: accounts.NewAccountIDs(acctID).
ShopID(platform, shopID).
ListingID(listingID),
Name: name,
SKU: sku,
Description: description,
Count: int64(count),
},
)
if err != nil {
return nil, fmt.Errorf("failed to update listing: %w", err)
}
return response.StatusOK(), nil
}
// DELETE /:acctID/platforms/:platform/shops/mocks/:shop-id/listings/:listing-id
func (s *accountSubrouter) deleteMockListing(c *gin.Context) (response.Response, error) {
acctID := auth.GetIdentity(c).Account.AccountID
var (
platform accounts.Platform
shopID string
listingID string
)
err := param.Path("platform", param.Platform(&platform)).
Path("shop-id", param.Text(&shopID)).
Path("listing-id", param.Text(&listingID)).
Unmarshal(c)
if err != nil {
return nil, err
}
err = s.accts.DeleteMockListing(
c,
accounts.NewAccountIDs(acctID).
ShopID(platform, shopID).
ListingID(listingID),
)
if err != nil {
return nil, fmt.Errorf("failed to delete listing: %w", err)
}
return response.StatusNoContent(), nil
}
// POST /:acctID/inventory/sync-groups/mock/draft/listings
func (s *accountSubrouter) createMockSyncGroupListingDraft(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := auth.GetIdentity(ctx).Account.AccountID
if _, err := s.accts.CreateMockSyncGroupListingDraft(ctx, acctID); err != nil {
return nil, response.Errorf("failed to create new listing draft: %w", err)
}
return response.StatusCreated(), nil
}
// PUT /:acctID/inventory/sync-groups/mock/draft/listings/:orderIndex/shop
// @platform string
// @shopID string
func (s *accountSubrouter) setShopInMockSyncGroupListingDraft(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := auth.GetIdentity(ctx).Account.AccountID
var (
orderIndex int
platform accounts.Platform
shopID string
)
err := param.Path("orderIndex", param.Int(&orderIndex)).
Form("platform", param.Platform(&platform)).
Form("shop-id", param.Text(&shopID)).
Unmarshal(c)
if err != nil {
return nil, err
}
if err := s.accts.SetShopInMockSyncGroupListingDraft(ctx, acctID, orderIndex, platform, shopID); err != nil {
return nil, response.Errorf("failed to set shop: %w", response.ErrorFromConstant(err))
}
return response.StatusOK(), nil
}
// PUT /:acctID/inventory/sync-groups/mock/draft/listings/:orderIndex/listing
func (s *accountSubrouter) setListingInMockSyncGroupListingDraft(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := auth.GetIdentity(ctx).Account.AccountID
var (
orderIndex int
listingID string
)
err := param.Path("orderIndex", param.Int(&orderIndex)).
Form("listing-id", param.Text(&listingID)).
Unmarshal(c)
if err != nil {
return nil, err
}
if err := s.accts.SetListingInMockSyncGroupListingDraft(ctx, acctID, orderIndex, listingID); err != nil {
return nil, response.Errorf("failed to set listing: %w", response.ErrorFromConstant(err))
}
return response.StatusOK(), nil
}
// DELETE /:acctID/inventory/sync-groups/mock/draft/listings/:orderIndex
func (s *accountSubrouter) deleteMockSyncGroupListingDraft(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := auth.GetIdentity(ctx).Account.AccountID
var (
orderIndex int
)
err := param.Path("orderIndex", param.Int(&orderIndex)).
Unmarshal(c)
if err != nil {
return nil, err
}
if err := s.accts.DeleteMockSyncGroupListingDraft(ctx, acctID, orderIndex); err != nil {
return nil, response.Errorf("failed to delete listing: %w", response.ErrorFromConstant(err))
}
return response.StatusOK(), nil
}
func lowerSnakeCase(s accounts.Platform) string {
return strings.ToLower(strings.Join(strings.Split(string(s), " "), "_"))
}
+56
View File
@@ -0,0 +1,56 @@
package api
import (
"github.com/gin-gonic/gin"
"ruben/inventory2/domains/accounts"
etsy_platform "ruben/inventory2/domains/platforms/etsy"
"ruben/inventory2/domains/raw_events"
"ruben/inventory2/logging"
accounts_api "ruben/inventory2/server/api/accounts"
auth_api "ruben/inventory2/server/api/auth"
sse_api "ruben/inventory2/server/api/sse"
"ruben/inventory2/server/api/webhooks"
etsy_webhooks "ruben/inventory2/server/api/webhooks/etsy"
"ruben/inventory2/server/auth"
"ruben/inventory2/server/sse"
)
func Routes(
r *gin.RouterGroup,
logger *logging.Logger,
auth *auth.Auth,
sq *sse.Queue,
accts *accounts.Store,
unp *sse.UpdateNotificationPublisher,
rawEvents *raw_events.Store,
etsy *etsy_platform.Platform,
) {
auth_api.Routes(
r.Group("/auth"),
logger.WithGroup("/auth"),
auth.GetAuthenticator(),
)
sse_api.Routes(
r.Group("/events", auth.Authenticate()),
logger.WithGroup("/events"),
sq,
)
accounts_api.Routes(
r.Group("/accounts", auth.Authenticate()),
logger.WithGroup("/accounts"),
accts,
unp.Group("/accounts"),
)
webhooks.Routes(
r.Group("/webhooks"),
logger.WithGroup("/webhooks"),
rawEvents,
etsy,
webhooks.Config{
Etsy: etsy_webhooks.Config{
OAuthRedirectURIWithAcctIDParam: "/oauth/accounts/:acctID/auth_code",
},
},
)
}
+94
View File
@@ -0,0 +1,94 @@
package auth
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"ruben/inventory2/domains/authentication"
"ruben/inventory2/logging"
"ruben/inventory2/server/cookies"
"ruben/inventory2/server/response"
)
type loginSubrouter struct {
log *logging.Logger
auth *authentication.Authenticator
}
func Routes(
r *gin.RouterGroup,
logger *logging.Logger,
auth *authentication.Authenticator,
) {
ls := &loginSubrouter{
log: logger,
auth: auth,
}
r.GET("/login", response.Handler(ls.loginPage))
r.GET("/login/callback", response.Handler(ls.loginCallback))
r.GET("/logout", response.Handler(ls.logoutPage))
}
func (s *loginSubrouter) loginPage(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
u, err := NewLoginURL(ctx, s.auth, "/")
if err != nil {
return nil, err
}
return response.TemporaryRedirect(u), nil
}
func NewLoginURL(ctx context.Context, auth *authentication.Authenticator, targetURI string) (string, error) {
state, err := auth.NewState(ctx, targetURI)
if err != nil {
return "", fmt.Errorf("failed to generate random state: %w", err)
}
base64EncodedState := fmt.Sprintf("%x", state[:])
return auth.AuthCodeURL(base64EncodedState), nil
}
func (s *loginSubrouter) loginCallback(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
q := r.URL.Query()
// obtain token and profile
accessToken, targetURI, expiration, err := s.auth.Exchange(ctx, q.Get("state"), q.Get("code"))
if err != nil {
return nil, response.Unauthorized().
Msg(fmt.Sprintf("Failed to exchange an authorization code for a token")).
Wrap(err)
}
// set access_token cookie and redirect to a reasonable place
return response.TemporaryRedirect(targetURI).
Cookie(cookies.AccessToken(accessToken, expiration)), nil
}
func (s *loginSubrouter) logoutPage(c *gin.Context) (response.Response, error) {
r := c.Request
host := r.Header.Get("X-Forwarded-Host")
if host == "" {
host = r.Host
}
if ck, err := r.Cookie("access_token"); err == nil && ck != nil {
if err := s.auth.DeleteOAuthTokens(r.Context(), ck.Value); err != nil {
s.log.Error("failed to delete auth token", "error", err)
}
}
return response.TemporaryRedirect(s.auth.GetLogoutURL(host).String()).
Cookie(cookies.Expired("access_token")), nil
}
+118
View File
@@ -0,0 +1,118 @@
package sse
import (
"context"
"sync"
"github.com/gin-gonic/gin"
"ruben/inventory2/logging"
"ruben/inventory2/server/auth"
"ruben/inventory2/server/response"
"ruben/inventory2/server/sse"
)
const (
maxNumOpenConnectionsPerUser = 3
)
type (
sseRouter struct {
log *logging.Logger
sse *sse.Queue
users map[string][maxNumOpenConnectionsPerUser]context.CancelFunc
lock sync.Mutex
}
)
func Routes(
r gin.IRouter,
logger *logging.Logger,
sq *sse.Queue,
) {
s := &sseRouter{
log: logger,
sse: sq,
users: make(map[string][maxNumOpenConnectionsPerUser]context.CancelFunc),
}
r.GET("/", response.Handler(s.serveEvents))
}
func (r *sseRouter) serveEvents(c *gin.Context) (response.Response, error) {
acct := auth.GetIdentity(c).Account
acctID := acct.AccountID
userID := acct.UserID
email := acct.Email
log := r.log.WithGroup("serveEvents").With(
"accountID", acctID,
"userID", userID,
"email", email,
"userAgent", c.Request.UserAgent(),
)
log.Info("user connected to sse queue")
ctx := r.closeOutstandingConnectionsForUserAndStoreCancelFuncForUser(c, userID)
w := c.Writer
hdr := w.Header()
hdr.Set("Access-Control-Allow-Origin", "*")
hdr.Set("Access-Control-Expose-Headers", "Content-Type")
hdr.Set("Content-Type", "text/event-stream")
hdr.Set("Connection", "keep-alive")
hdr.Set("Cache-Control", "no-cache")
w.Flush()
err := r.sse.Listen(ctx, acctID, func(ctx context.Context, e *sse.Event) error {
log.Debugf("sending event of type %s", e.Type)
e.Write(w)
return nil
})
if err != nil {
log.Errorf("no longer connected to sse queue due to error: %v", err)
return nil, err
}
// send a 'close' message so the front end doesn't try to reconnect
(&sse.Event{Type: "close"}).Write(w)
log.Info("user disconnecting sse queue")
return response.Status(200), nil
}
func (r *sseRouter) closeOutstandingConnectionsForUserAndStoreCancelFuncForUser(ctx context.Context, userID string) context.Context {
r.lock.Lock()
defer r.lock.Unlock()
ctx, cancel := context.WithCancel(ctx)
closeConnFuncs := r.users[userID]
defer func() {
r.users[userID] = closeConnFuncs
}()
// close existing connection
for i := range maxNumOpenConnectionsPerUser {
if fn := closeConnFuncs[i]; fn == nil {
// not hit limit on connections.
// save the cancellation func and done
closeConnFuncs[i] = cancel
return ctx
}
}
// close the oldest connection, shift all cancellation funcs down, and push the new one in
closeConnFuncs[0]()
for i := range maxNumOpenConnectionsPerUser - 1 {
closeConnFuncs[i] = closeConnFuncs[i+1]
}
closeConnFuncs[maxNumOpenConnectionsPerUser-1] = cancel
return ctx
}
+152
View File
@@ -0,0 +1,152 @@
package etsy
import (
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/gin-gonic/gin"
"ruben/inventory2/domains/platforms/etsy"
"ruben/inventory2/domains/raw_events"
"ruben/inventory2/logging"
"ruben/inventory2/server/response"
)
type (
Config struct {
OAuthRedirectURIWithAcctIDParam string
}
)
func Routes(
r *gin.RouterGroup,
logger *logging.Logger,
db *raw_events.Store,
platform *etsy.Platform,
cfg Config,
) {
h := webhooks{
log: logger,
cfg: cfg,
db: db,
etsy: platform,
}
r.POST("/test", response.Handler(h.test))
r.GET(h.cfg.OAuthRedirectURIWithAcctIDParam, response.Handler(h.redirectURI))
r.GET("/:acctID/new-account-link", response.Handler(h.newAccountLink))
}
type (
webhooks struct {
log *logging.Logger
cfg Config
db *raw_events.Store
etsy *etsy.Platform
}
)
// POST /test
func (h webhooks) test(c *gin.Context) (response.Response, error) {
r := c.Request
var body json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return nil, response.Errorf("failed to decode body as json: %w", err)
}
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 {
return nil, response.Errorf("error occurred saving the body as the event payload: %w", err)
}
return response.Status(201), nil
}
// GET h.cfg.OAuthRedirectURIWithAcctIDParam
func (h webhooks) redirectURI(c *gin.Context) (response.Response, error) {
r := c.Request
// get account id for the request
acctID, err := strconv.ParseInt(c.Param("acctID"), 10, 64)
if err != nil || acctID <= 0 {
return nil, response.NotFound()
}
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 response.Status(200), nil
}
// validate the state to prevent CSRF attacks
ok, err := h.etsy.HandleNewAuthCode(ctx, acctID, state, q.Get("code"))
if err != nil {
h.log.Error("failed to handle new auth code", "error", err)
return nil, response.Forbidden()
}
if !ok {
return nil, response.Forbidden()
}
// redirect to the user's account page
return response.SeeOther(fmt.Sprintf("/accounts/%d", acctID)), nil
}
// GET /:acctID/new-account-link
func (h webhooks) newAccountLink(c *gin.Context) (response.Response, error) {
r := c.Request
acctIDStr := c.Param("acctID")
acctID, err := strconv.ParseInt(acctIDStr, 10, 64)
if err != nil {
return nil, response.NotFound().Msgf("account %s not found", acctIDStr)
}
u, err := h.etsy.GenerateConnectionURLForNewAccount(r.Context(), acctID)
if err != nil {
return nil, fmt.Errorf("failed to generate url for account %d: %w", acctID, err)
}
return response.TemporaryRedirect(u.String()), nil
}
+43
View File
@@ -0,0 +1,43 @@
package tiktok
import (
"encoding/json"
"fmt"
"time"
"github.com/gin-gonic/gin"
"ruben/inventory2/domains/raw_events"
"ruben/inventory2/logging"
"ruben/inventory2/server/response"
)
func Routes(
r *gin.RouterGroup,
logger *logging.Logger,
db *raw_events.Store,
) {
r.POST("/test", response.Handler(func(c *gin.Context) (response.Response, error) {
r := c.Request
var body json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return nil, fmt.Errorf("failed to decode body as json: %w", err)
}
ts := time.Now().UTC()
err := db.Save(r.Context(), &raw_events.Event{
Platform: "tiktok",
StoreID: "test-store-1",
EventID: fmt.Sprint(ts.Unix()),
EventTimestamp: ts,
Payload: body,
})
if err != nil {
return nil, fmt.Errorf("error occurred saving the body as the event payload: %w", err)
}
return response.Status(201), nil
}))
}
+42
View File
@@ -0,0 +1,42 @@
package webhooks
import (
"github.com/gin-gonic/gin"
etsy_platform "ruben/inventory2/domains/platforms/etsy"
"ruben/inventory2/domains/raw_events"
"ruben/inventory2/logging"
"ruben/inventory2/server/api/webhooks/etsy"
"ruben/inventory2/server/api/webhooks/tiktok"
"ruben/inventory2/server/api/webhooks/wix"
)
type Config struct {
Etsy etsy.Config
}
func Routes(
r *gin.RouterGroup,
logger *logging.Logger,
eventsDB *raw_events.Store,
etsyPlatform *etsy_platform.Platform,
cfg Config,
) {
etsy.Routes(
r.Group("/etsy"),
logger.WithGroup("etsy"),
eventsDB,
etsyPlatform,
cfg.Etsy,
)
tiktok.Routes(
r.Group("/tiktok"),
logger.WithGroup("tiktok"),
eventsDB,
)
wix.Routes(
r.Group("/wix"),
logger.WithGroup("wix"),
eventsDB,
)
}
+43
View File
@@ -0,0 +1,43 @@
package wix
import (
"encoding/json"
"fmt"
"time"
"github.com/gin-gonic/gin"
"ruben/inventory2/domains/raw_events"
"ruben/inventory2/logging"
"ruben/inventory2/server/response"
)
func Routes(
r *gin.RouterGroup,
logger *logging.Logger,
db *raw_events.Store,
) {
r.POST("/test", response.Handler(func(c *gin.Context) (response.Response, error) {
r := c.Request
var body json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return nil, fmt.Errorf("failed to decode body as json: %w", err)
}
ts := time.Now().UTC()
err := db.Save(r.Context(), &raw_events.Event{
Platform: "wix",
StoreID: "test-store-1",
EventID: fmt.Sprint(ts.Unix()),
EventTimestamp: ts,
Payload: body,
})
if err != nil {
return nil, fmt.Errorf("error occurred saving the body as the event payload: %w", err)
}
return response.Status(201), nil
}))
}