removed intermediate /internal directory
This commit is contained in:
@@ -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), " "), "_"))
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}))
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user