installed gin

This commit is contained in:
2026-01-13 22:07:20 -07:00
parent 851234d9fa
commit 7481cec0d5
29 changed files with 544 additions and 1094 deletions
+38 -31
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"ruben/inventory2/internal/consts"
"slices"
"github.com/jackc/pgx/v5"
)
@@ -18,7 +19,8 @@ type (
SyncGroupListing struct {
SyncGroupIDs
AccountShopIDs
ListingID string
ListingID string
OrderIndex int
}
SyncGroupIDs struct {
@@ -28,7 +30,8 @@ type (
SyncGroupListingDraft struct {
AccountShopIDs
ListingID string
ListingID string
OrderIndex int
}
)
@@ -96,9 +99,10 @@ func (db *Store) GetSyncGroupListingDraft(ctx context.Context, acctID int64, ord
}
r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[struct {
Platform *Platform
Shop_id *string
Listing_id *string
Platform *Platform
Shop_id *string
Listing_id *string
Order_index *int
}])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
@@ -115,7 +119,8 @@ func (db *Store) GetSyncGroupListingDraft(ctx context.Context, acctID int64, ord
Platform: deref(r.Platform),
ShopID: deref(r.Shop_id),
},
ListingID: deref(r.Listing_id),
ListingID: deref(r.Listing_id),
OrderIndex: orderIndex,
}, nil
}
@@ -182,17 +187,7 @@ func (db *Store) DeleteSyncGroupListingDraft(ctx context.Context, acctID int64,
rows, err := db.db.Query(
ctx,
`
WITH updated_drafts AS (
UPDATE
sync_group_listing_drafts
SET
order_index = (order_index - 1)
WHERE
account_id = @account_id
AND order_index > @order_index
RETURNING
order_index
), deleted_draft AS (
WITH deleted_draft AS (
DELETE FROM
sync_group_listing_drafts
WHERE
@@ -202,17 +197,17 @@ func (db *Store) DeleteSyncGroupListingDraft(ctx context.Context, acctID int64,
true AS found
)
SELECT
COALESCE(dd.found, false) AS found,
(COALESCE(MAX(ud.order_index), -1) + 1) AS num_rows
COUNT(*) as num_rows,
COALESCE(dd.found, FALSE) as found
FROM
deleted_draft dd
sync_group_listing_drafts ld
LEFT JOIN
updated_drafts ud
ON true
deleted_draft dd
ON TRUE
WHERE
account_id = @account_id
GROUP BY
ud.order_index, dd.found
LIMIT
1
ld.account_id, dd.found
`,
pgx.NamedArgs{
"account_id": acctID,
@@ -256,6 +251,8 @@ func (db *Store) GetSyncGroupListingDrafts(ctx context.Context, acctID int64) ([
sync_group_listing_drafts
WHERE
account_id = @account_id
ORDER BY
order_index ASC
`,
pgx.NamedArgs{
"account_id": acctID,
@@ -278,8 +275,8 @@ func (db *Store) GetSyncGroupListingDrafts(ctx context.Context, acctID int64) ([
}
listings := make([]SyncGroupListingDraft, len(rs))
for _, r := range rs {
listings[r.Order_index] = SyncGroupListingDraft{
for i, r := range rs {
listings[i] = SyncGroupListingDraft{
AccountShopIDs: AccountShopIDs{
AccountIDs: AccountIDs{
AccountID: acctID,
@@ -287,10 +284,15 @@ func (db *Store) GetSyncGroupListingDrafts(ctx context.Context, acctID int64) ([
Platform: deref(r.Platform),
ShopID: deref(r.Shop_id),
},
ListingID: deref(r.Listing_id),
ListingID: deref(r.Listing_id),
OrderIndex: r.Order_index,
}
}
slices.SortFunc(listings, func(a, b SyncGroupListingDraft) int {
return a.OrderIndex - b.OrderIndex
})
return listings, nil
}
@@ -397,8 +399,8 @@ func (db *Store) SaveNewSyncGroup(ctx context.Context, acctID int64) (SyncGroup,
}
listings := make([]SyncGroupListing, len(rs))
for _, r := range rs {
listings[r.Order_index] = SyncGroupListing{
for i, r := range rs {
listings[i] = SyncGroupListing{
SyncGroupIDs: SyncGroupIDs{
AccountIDs: AccountIDs{
AccountID: acctID,
@@ -412,10 +414,15 @@ func (db *Store) SaveNewSyncGroup(ctx context.Context, acctID int64) (SyncGroup,
Platform: r.Platform,
ShopID: r.Shop_id,
},
ListingID: r.Listing_id,
ListingID: r.Listing_id,
OrderIndex: r.Order_index,
}
}
slices.SortFunc(listings, func(a, b SyncGroupListing) int {
return a.OrderIndex - b.OrderIndex
})
return SyncGroup{
SyncGroupIDs: SyncGroupIDs{
AccountIDs: AccountIDs{
+1 -1
View File
@@ -31,7 +31,7 @@ const (
AUTH0_CLIENT_SECRET = "83U-iWdVaNnwk9XDzteo_2VMyOq_l1siKYqg1_2E7jCzgL8MnkaxlysPMcPMGlxA"
// The Callback URL of our application.
AUTH0_CALLBACK_URL = "https://inventory-plus-plus.com/auth/login/callback"
AUTH0_CALLBACK_URL = "https://inventory-plus-plus.com/api/auth/login/callback"
)
type (
+43 -41
View File
@@ -9,44 +9,41 @@ import (
"ruben/inventory2/internal/logging"
"ruben/inventory2/internal/server/middleware"
"ruben/inventory2/internal/server/response"
"ruben/inventory2/internal/server/router"
"strconv"
"github.com/gin-gonic/gin"
)
type accountSubrouter struct {
log *logging.Logger
*router.SubMux
log *logging.Logger
accts *accounts.Store
}
func NewAccountSubrouter(
func Routes(
r *gin.RouterGroup,
logger *logging.Logger,
accts *accounts.Store,
authMiddleware *middleware.Auth,
) *accountSubrouter {
mux := router.NewSubMux(logger)
) {
as := &accountSubrouter{
log: logger,
SubMux: mux,
accts: accts,
log: logger,
accts: accts,
}
withAuth := func(fn response.HandlerFunc) response.HandlerFunc {
return authMiddleware.AuthenticateAndAddIdentity(fn)
withAuth := func(fn response.HandlerFunc) gin.HandlerFunc {
return response.Handler(authMiddleware.AuthenticateAndAddIdentity(fn))
}
mux.Handle("POST /{acctID}/inventory/sync-groups/draft/listings", withAuth(as.createSyncGroupListingDraft))
mux.Handle("PUT /{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/shop", withAuth(as.setShopInSyncGroupListingDraft))
mux.Handle("PUT /{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/listing", withAuth(as.setListingInSyncGroupListingDraft))
mux.Handle("DELETE /{acctID}/inventory/sync-groups/draft/listings/{orderIndex}", withAuth(as.deleteSyncGroupListingDraft))
mux.Handle("POST /{acctID}/inventory/sync-groups", withAuth(as.saveNewSyncGroup))
return as
r.POST("/:acctID/inventory/sync-groups/draft/listings", withAuth(as.createSyncGroupListingDraft))
r.PUT("/:acctID/inventory/sync-groups/draft/listings/:orderIndex/shop", withAuth(as.setShopInSyncGroupListingDraft))
r.PUT("/:acctID/inventory/sync-groups/draft/listings/:orderIndex/listing", withAuth(as.setListingInSyncGroupListingDraft))
r.DELETE("/:acctID/inventory/sync-groups/draft/listings/:orderIndex", withAuth(as.deleteSyncGroupListingDraft))
r.POST("/:acctID/inventory/sync-groups", withAuth(as.saveNewSyncGroup))
}
// POST /accounts
func (s *accountSubrouter) createAccount(r *http.Request) (response.Response, error) {
// POST /api/accounts
func (s *accountSubrouter) createAccount(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
email := r.FormValue("email")
if email == "" {
@@ -64,11 +61,12 @@ func (s *accountSubrouter) createAccount(r *http.Request) (response.Response, er
return nil, response.Errorf("failed to create account: %w", err)
}
return response.SeeOther(fmt.Sprintf("/accounts/%d", acct.AccountID)), nil
return response.SeeOther(fmt.Sprintf("/api/accounts/%d", acct.AccountID)), nil
}
// POST /accounts/{acctID}/inventory/sync-groups/draft/listings
func (s *accountSubrouter) createSyncGroupListingDraft(r *http.Request) (response.Response, error) {
// POST /api/accounts/{acctID}/inventory/sync-groups/draft/listings
func (s *accountSubrouter) createSyncGroupListingDraft(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := middleware.GetIdentity(ctx).Account.AccountID
@@ -79,18 +77,19 @@ func (s *accountSubrouter) createSyncGroupListingDraft(r *http.Request) (respons
return response.Redirect(
http.StatusSeeOther,
fmt.Sprintf("/accounts/%d/inventory/sync-groups/draft/listings/%d", acctID, orderIndex),
fmt.Sprintf("/ui/accounts/%d/inventory/sync-groups/draft/listings/%d", acctID, orderIndex),
), nil
}
// PUT /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/shop
// PUT /api/accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/shop
// @platform string
// @shopID string
func (s *accountSubrouter) setShopInSyncGroupListingDraft(r *http.Request) (response.Response, error) {
func (s *accountSubrouter) setShopInSyncGroupListingDraft(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := middleware.GetIdentity(ctx).Account.AccountID
orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(r)
orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(c)
if err != nil {
return nil, err
}
@@ -114,16 +113,17 @@ func (s *accountSubrouter) setShopInSyncGroupListingDraft(r *http.Request) (resp
return response.Redirect(
http.StatusSeeOther,
fmt.Sprintf("/accounts/%d/inventory/sync-groups/draft/listings/%d", acctID, orderIndex),
fmt.Sprintf("/ui/accounts/%d/inventory/sync-groups/draft/listings/%d", acctID, orderIndex),
), nil
}
// PUT /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/listing
func (s *accountSubrouter) setListingInSyncGroupListingDraft(r *http.Request) (response.Response, error) {
// PUT /api/accounts/{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 := middleware.GetIdentity(ctx).Account.AccountID
orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(r)
orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(c)
if err != nil {
return nil, err
}
@@ -140,16 +140,17 @@ func (s *accountSubrouter) setListingInSyncGroupListingDraft(r *http.Request) (r
return response.Redirect(
http.StatusSeeOther,
fmt.Sprintf("/accounts/%d/inventory/sync-groups/draft/listings/%d", acctID, orderIndex),
fmt.Sprintf("/ui/accounts/%d/inventory/sync-groups/draft/listings/%d", acctID, orderIndex),
), nil
}
// DELETE /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}
func (s *accountSubrouter) deleteSyncGroupListingDraft(r *http.Request) (response.Response, error) {
// DELETE /api/accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}
func (s *accountSubrouter) deleteSyncGroupListingDraft(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := middleware.GetIdentity(ctx).Account.AccountID
orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(r)
orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(c)
if err != nil {
return nil, err
}
@@ -161,8 +162,9 @@ func (s *accountSubrouter) deleteSyncGroupListingDraft(r *http.Request) (respons
return response.Status(200), nil
}
// POST /accounts/{acctID}/inventory/sync-groups
func (s *accountSubrouter) saveNewSyncGroup(r *http.Request) (response.Response, error) {
// POST /api/accounts/{acctID}/inventory/sync-groups
func (s *accountSubrouter) saveNewSyncGroup(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := middleware.GetIdentity(ctx).Account.AccountID
@@ -174,12 +176,12 @@ func (s *accountSubrouter) saveNewSyncGroup(r *http.Request) (response.Response,
return response.Redirect(
http.StatusSeeOther,
// TODO: template not implemented
fmt.Sprintf("/accounts/%d/inventory/sync-groups/%d", acctID, grp.SyncGroupID),
fmt.Sprintf("/ui/accounts/%d/inventory/sync-groups/%d", acctID, grp.SyncGroupID),
), nil
}
func getOrderIndexForSyncGroupListingDraftFromPath(r *http.Request) (int, error) {
orderIndexStr := r.PathValue("orderIndex")
func getOrderIndexForSyncGroupListingDraftFromPath(c *gin.Context) (int, error) {
orderIndexStr := c.Param("orderIndex")
if orderIndexStr == "" {
return 0, response.NotFound().
Msg("no orderIndex found")
+19 -33
View File
@@ -3,53 +3,36 @@ package auth
import (
"context"
"fmt"
"net/http"
"ruben/inventory2/internal/domains/authentication"
"ruben/inventory2/internal/logging"
"ruben/inventory2/internal/server/cookies"
"ruben/inventory2/internal/server/response"
"ruben/inventory2/internal/server/router"
"github.com/gin-gonic/gin"
)
type loginSubrouter struct {
log *logging.Logger
auth *authentication.Authenticator
router.Subrouter
}
func NewLoginSubrouter(logger *logging.Logger, auth *authentication.Authenticator) *loginSubrouter {
mux := router.NewSubMux(logger)
func Routes(
r *gin.RouterGroup,
logger *logging.Logger,
auth *authentication.Authenticator,
) {
ls := &loginSubrouter{
log: logger,
auth: auth,
Subrouter: mux,
log: logger,
auth: auth,
}
mux.Handle("GET /login", ls.loginPage)
mux.Handle("GET /login/callback", ls.loginCallback)
mux.Handle("GET /logout", ls.logoutPage)
return ls
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) newLoginSubrouter(logger *logging.Logger) router.Subrouter {
mux := router.NewSubMux(logger)
ls := &loginSubrouter{
log: s.log,
auth: s.auth,
Subrouter: mux,
}
mux.Handle("GET /login", ls.loginPage)
mux.Handle("GET /login/callback", ls.loginCallback)
mux.Handle("GET /logout", ls.logoutPage)
return ls
}
func (s *loginSubrouter) loginPage(r *http.Request) (response.Response, error) {
func (s *loginSubrouter) loginPage(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
u, err := NewLoginURL(ctx, s.auth, "/")
@@ -71,7 +54,8 @@ func NewLoginURL(ctx context.Context, auth *authentication.Authenticator, target
return auth.AuthCodeURL(base64EncodedState), nil
}
func (s *loginSubrouter) loginCallback(r *http.Request) (response.Response, error) {
func (s *loginSubrouter) loginCallback(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
q := r.URL.Query()
@@ -90,7 +74,9 @@ func (s *loginSubrouter) loginCallback(r *http.Request) (response.Response, erro
Cookie(cookies.AccessToken(accessToken, expiration)), nil
}
func (s *loginSubrouter) logoutPage(r *http.Request) (response.Response, error) {
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
+61 -29
View File
@@ -17,55 +17,74 @@ import (
"ruben/inventory2/internal/logging"
"ruben/inventory2/internal/server/middleware"
"ruben/inventory2/internal/server/response"
"ruben/inventory2/internal/server/router"
"github.com/angelbeltran/templater"
"github.com/gin-gonic/gin"
)
type webpageRouter struct {
log *logging.Logger
contentDir string
templater *templater.Templater
rawEvents *raw_events.Store
accts *accounts.Store
etsy *etsy_platform.Platform
router.Subrouter
}
type (
webpageRouter struct {
log *logging.Logger
contentDir string
templater *templater.Templater
rawEvents *raw_events.Store
accts *accounts.Store
etsy *etsy_platform.Platform
authMiddleware *middleware.Auth
}
func NewWebpageRouter(
// ErrTemplateNotFound is returned if the reason the template failed to compile
// is due to the template not being found.
ErrTemplateNotFound struct {
err error
}
)
func SetupRoutes(
logger *logging.Logger,
r gin.IRouter,
contentDir string,
tmpl *templater.Templater,
rawEvents *raw_events.Store,
accts *accounts.Store,
etsy *etsy_platform.Platform,
authMiddleware *middleware.Auth,
) *webpageRouter {
mux := router.NewSubMux(logger)
wr := &webpageRouter{
log: logger,
contentDir: contentDir,
templater: tmpl,
rawEvents: rawEvents,
accts: accts,
etsy: etsy,
Subrouter: mux,
) {
s := &webpageRouter{
log: logger,
contentDir: contentDir,
templater: tmpl,
rawEvents: rawEvents,
accts: accts,
etsy: etsy,
authMiddleware: authMiddleware,
}
// non-authenticated
mux.Handle("GET /{$}", authMiddleware.AddIdentity(wr.serveTemplates))
fn2 := s.authMiddleware.AuthenticateAndAddIdentity(s.serveTemplates)
// authenticated
mux.Handle("GET /", authMiddleware.AuthenticateAndAddIdentity(wr.serveTemplates))
r.GET("/*rest", response.Handler(func(c *gin.Context) (response.Response, error) {
c.Request.URL.Path = c.Request.URL.Path[3:]
defer func() {
c.Request.URL.Path = "/ui" + c.Request.URL.Path
}()
return wr
p := c.Request.URL.Path
if p == "/" || p == "" {
c, err := s.authMiddleware.AddIdentityToRequest(c)
if err != nil {
return nil, err
}
return s.serveTemplates(c)
}
return fn2(c)
}))
}
// GET /
// compiles the page template or component template matching the url
// func (s *Server) serveTemplates(r *http.Request) (response.Response, error) {
func (s *webpageRouter) serveTemplates(r *http.Request) (response.Response, error) {
func (s *webpageRouter) serveTemplates(c *gin.Context) (response.Response, error) {
r := c.Request
name, args := s.getTemplateNameAndArgs(r, s.contentDir+"/templates/component_bodies")
b, err := s.templater.ExecuteComponentBody(name, args...)
@@ -190,7 +209,9 @@ func getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(file
func (s *webpageRouter) handleTemplateError(err error, templateArgs ...any) (response.Response, error) {
if isFileNotFoundError(err) {
return nil, response.NotFound().
Wrap(err).
Wrap(ErrTemplateNotFound{
err: err,
}).
Msg("resource not found")
}
@@ -280,3 +301,14 @@ func authorizeByMatchingAccountID(r *http.Request, acctIDPathPosition int) error
return nil
}
func (e ErrTemplateNotFound) Error() string {
if e.err != nil {
return fmt.Sprintf("template not found: %v", e.err)
}
return fmt.Sprintf("template not found")
}
func (e ErrTemplateNotFound) Unwrap() error {
return e.err
}
+40 -41
View File
@@ -3,58 +3,60 @@ package etsy
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"ruben/inventory2/internal/domains/platforms/etsy"
"ruben/inventory2/internal/domains/raw_events"
"ruben/inventory2/internal/logging"
"ruben/inventory2/internal/server/response"
"github.com/gin-gonic/gin"
)
type (
Webhooks struct {
log *logging.Logger
cfg Config
db *raw_events.Store
etsy *etsy.Platform
}
Config struct {
OAuthRedirectURIWithAcctIDParam string
}
)
func NewWebhookHandler(
func Webhooks(
r *gin.RouterGroup,
logger *logging.Logger,
db *raw_events.Store,
platform *etsy.Platform,
cfg Config,
) http.Handler {
h := Webhooks{
) {
h := webhooks{
log: logger,
cfg: cfg,
db: db,
etsy: platform,
}
mux := http.NewServeMux()
r.POST("/test", response.Handler(h.test))
mux.HandleFunc("POST /test", h.test)
r.GET(h.cfg.OAuthRedirectURIWithAcctIDParam, response.Handler(h.redirectURI))
mux.HandleFunc("GET "+h.cfg.OAuthRedirectURIWithAcctIDParam, h.redirectURI)
mux.HandleFunc("GET /{acctID}/new-account-link", h.newAccountLink)
return mux
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(w http.ResponseWriter, r *http.Request) {
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 {
http.Error(w, "Failed to decode body as json: "+err.Error(), 500)
return
return nil, response.Errorf("failed to decode body as json: %w", err)
}
ts := time.Now().UTC()
@@ -75,21 +77,21 @@ func (h Webhooks) test(w http.ResponseWriter, r *http.Request) {
Payload: body,
})
if err != nil {
http.Error(w, "Error occurred saving the body as the event payload: "+err.Error(), 500)
return
return nil, response.Errorf("error occurred saving the body as the event payload: %w", err)
}
w.WriteHeader(201)
return response.Status(201), nil
}
// GET h.cfg.OAuthRedirectURIWithAcctIDParam
func (h Webhooks) redirectURI(w http.ResponseWriter, r *http.Request) {
func (h webhooks) redirectURI(c *gin.Context) (response.Response, error) {
r := c.Request
// get account id for the request
acctID, err := strconv.ParseInt(r.PathValue("acctID"), 10, 64)
acctID, err := strconv.ParseInt(c.Param("acctID"), 10, 64)
if err != nil || acctID <= 0 {
w.WriteHeader(http.StatusNotFound)
return
return nil, response.NotFound()
}
ctx := r.Context()
@@ -113,41 +115,38 @@ func (h Webhooks) redirectURI(w http.ResponseWriter, r *http.Request) {
h.etsy.InvalidateState(ctx, state)
return
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 {
w.WriteHeader(http.StatusForbidden)
h.log.Error("failed to handle new auth code", "error", err)
return
return nil, response.Forbidden()
}
if !ok {
w.WriteHeader(http.StatusForbidden)
return
return nil, response.Forbidden()
}
// redirect to the user's account page
http.Redirect(w, r, fmt.Sprintf("/accounts/%d", acctID), http.StatusSeeOther)
return response.SeeOther(fmt.Sprintf("/accounts/%d", acctID)), nil
}
// GET /{acctID}/new-account-link
func (h Webhooks) newAccountLink(w http.ResponseWriter, r *http.Request) {
acctIDStr := r.PathValue("acctID")
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 {
http.Error(w, fmt.Sprintf("account %s not found", acctIDStr), http.StatusNotFound)
return
return nil, response.NotFound().Msgf("account %s not found", acctIDStr)
}
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
return nil, fmt.Errorf("failed to generate url for account %d: %w", acctID, err)
}
http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect)
return response.TemporaryRedirect(u.String()), nil
}
+14 -12
View File
@@ -3,21 +3,26 @@ package tiktok
import (
"encoding/json"
"fmt"
"net/http"
"time"
"ruben/inventory2/internal/domains/raw_events"
"ruben/inventory2/internal/logging"
"ruben/inventory2/internal/server/response"
"github.com/gin-gonic/gin"
)
func NewWebhookHandler(logger *logging.Logger, db *raw_events.Store) http.Handler {
mux := http.NewServeMux()
func Webhooks(
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
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
return nil, fmt.Errorf("failed to decode body as json: %w", err)
}
ts := time.Now().UTC()
@@ -30,12 +35,9 @@ func NewWebhookHandler(logger *logging.Logger, db *raw_events.Store) http.Handle
Payload: body,
})
if err != nil {
http.Error(w, "Error occurred saving the body as the event payload: "+err.Error(), 500)
return
return nil, fmt.Errorf("error occurred saving the body as the event payload: %w", err)
}
w.WriteHeader(201)
})
return mux
return response.Status(201), nil
}))
}
+23 -28
View File
@@ -1,47 +1,42 @@
package webhooks
import (
"net/http"
etsy_platform "ruben/inventory2/internal/domains/platforms/etsy"
"ruben/inventory2/internal/domains/raw_events"
"ruben/inventory2/internal/logging"
"ruben/inventory2/internal/server/api/webhooks/etsy"
"ruben/inventory2/internal/server/api/webhooks/tiktok"
"ruben/inventory2/internal/server/api/webhooks/wix"
"github.com/gin-gonic/gin"
)
type Config struct {
Etsy etsy.Config
}
// TODO: just move over to the 'site' package, and then consider renaming the site package to something else?
func New(logger *logging.Logger, eventsDB *raw_events.Store, etsyPlatform *etsy_platform.Platform, cfg Config) http.Handler {
wh := http.NewServeMux()
wh.Handle(
"/etsy/",
http.StripPrefix("/etsy", etsy.NewWebhookHandler(
logger.WithGroup("etsy"),
eventsDB,
etsyPlatform,
cfg.Etsy,
)),
func Webhooks(
r *gin.RouterGroup,
logger *logging.Logger,
eventsDB *raw_events.Store,
etsyPlatform *etsy_platform.Platform,
cfg Config,
) {
etsy.Webhooks(
r.Group("/etsy"),
logger.WithGroup("etsy"),
eventsDB,
etsyPlatform,
cfg.Etsy,
)
wh.Handle(
"/tiktok/",
http.StripPrefix("/tiktok", tiktok.NewWebhookHandler(
logger.WithGroup("tiktok"),
eventsDB,
)),
tiktok.Webhooks(
r.Group("/tiktok"),
logger.WithGroup("tiktok"),
eventsDB,
)
wh.Handle(
"/wix/",
http.StripPrefix("/wix", wix.NewWebhookHandler(
logger.WithGroup("wix"),
eventsDB,
)),
wix.Webhooks(
r.Group("/wix"),
logger.WithGroup("wix"),
eventsDB,
)
return wh
}
+14 -12
View File
@@ -3,21 +3,26 @@ package wix
import (
"encoding/json"
"fmt"
"net/http"
"time"
"ruben/inventory2/internal/domains/raw_events"
"ruben/inventory2/internal/logging"
"ruben/inventory2/internal/server/response"
"github.com/gin-gonic/gin"
)
func NewWebhookHandler(logger *logging.Logger, db *raw_events.Store) http.Handler {
mux := http.NewServeMux()
func Webhooks(
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
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
return nil, fmt.Errorf("failed to decode body as json: %w", err)
}
ts := time.Now().UTC()
@@ -30,12 +35,9 @@ func NewWebhookHandler(logger *logging.Logger, db *raw_events.Store) http.Handle
Payload: body,
})
if err != nil {
http.Error(w, "Error occurred saving the body as the event payload: "+err.Error(), 500)
return
return nil, fmt.Errorf("error occurred saving the body as the event payload: %w", err)
}
w.WriteHeader(201)
})
return mux
return response.Status(201), nil
}))
}
+92 -74
View File
@@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"io"
"net/http"
"time"
"ruben/inventory2/internal/consts"
@@ -15,6 +14,8 @@ import (
"ruben/inventory2/internal/logging"
"ruben/inventory2/internal/server/cookies"
"ruben/inventory2/internal/server/response"
"github.com/gin-gonic/gin"
)
type (
@@ -52,20 +53,22 @@ func NewAuth(
}
func (a *Auth) AddIdentity(fn response.HandlerFunc) response.HandlerFunc {
return func(r *http.Request) (response.Response, error) {
r, err := a.AddIdentityToRequest(r)
return func(c *gin.Context) (response.Response, error) {
c, err := a.AddIdentityToRequest(c)
if err != nil {
return nil, err
}
return fn(r)
return fn(c)
}
}
func (a *Auth) AddIdentityToRequest(r *http.Request) (*http.Request, error) {
func (a *Auth) AddIdentityToRequest(c *gin.Context) (*gin.Context, error) {
r := c.Request
ck, err := r.Cookie("access_token")
if err != nil {
return r, nil
return c, nil
}
ctx := r.Context()
@@ -75,27 +78,29 @@ func (a *Auth) AddIdentityToRequest(r *http.Request) (*http.Request, error) {
claims, expiration, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
if err != nil {
if errors.Is(err, consts.ErrNotFound) {
return r, nil
return c, nil
}
return r, response.Errorf("failed to load authentication details: %w", err)
return c, response.Errorf("failed to load authentication details: %w", err)
}
if expiration.Before(time.Now()) {
return r, nil
return c, nil
}
user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken)
if err != nil {
return r, response.Errorf("failed to load user and account defails: %w", err)
return c, response.Errorf("failed to load user and account defails: %w", err)
}
return r.WithContext(SetIdentity(ctx, Identity{
c.Request = r.WithContext(SetIdentity(ctx, Identity{
AccessToken: accessToken,
Claims: claims,
User: user,
Account: acct,
})), nil
}))
return c, nil
}
// TODO: after getting auth, consider making http handler functions take 'claims', etc, as function arguments
@@ -103,73 +108,86 @@ func (a *Auth) AddIdentityToRequest(r *http.Request) (*http.Request, error) {
// auth middleware to verify access_token cookie and set custom claims in the request context
func (a *Auth) AuthenticateAndAddIdentity(f response.HandlerFunc, assertions ...AuthorizationAssertions) response.HandlerFunc {
return func(r *http.Request) (response.Response, error) {
ck, err := r.Cookie("access_token")
if err != nil {
return response.TemporaryRedirect("/").
JSON("no access_token cookie provided"), nil
return func(c *gin.Context) (response.Response, error) {
c, res, err := a.AuthenticateAndAddIdentityToRequest(c, assertions...)
if res != nil || err != nil {
return res, nil
}
ctx := r.Context()
accessToken := ck.Value
claims, expiration, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
if err != nil {
if errors.Is(err, consts.ErrNotFound) {
u, err := a.newLoginURL(ctx, a.auth, r.URL.String())
if err != nil {
return nil, response.Errorf("failed to generate login url: %w", err)
}
return response.TemporaryRedirect(u), nil
}
return nil, response.Errorf("failed to authenticate: %w", err)
}
now := time.Now()
// refresh tokens, when the access token is "old enough"
// id token lifetime is 48 hours, allowing a person to use the app everyday comfortably, with wiggle room, without having to log in.
const idTokenLifetime = 48 * time.Hour
if refreshFloor := expiration.Add(-(idTokenLifetime / 4)); refreshFloor.Before(now) {
accessToken, expiration, err = a.auth.RefreshAccessToken(ctx, accessToken)
if err != nil {
a.log.Warn("failed to refresh access token", "error", err)
return response.TemporaryRedirect("/").
Body(io.NopCloser(bytes.NewBuffer([]byte(fmt.Sprintf("failed to refresh access token: %v", err))))).
Cookie(cookies.Expired("access_token")), nil
}
// 'redirect' to same url, to set the new access_token cookie
return response.TemporaryRedirect(r.URL.String()).
Cookie(cookies.AccessToken(accessToken, expiration)), nil
}
// add identity info to request context
user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken)
if err != nil {
return nil, response.Errorf("failed to authorize: %w", err)
}
for _, as := range assertions {
if res, err := as(r); res != nil || err != nil {
return res, err
}
}
return f(r.WithContext(SetIdentity(ctx, Identity{
AccessToken: accessToken,
Claims: claims,
User: user,
Account: acct,
})))
return f(c)
}
}
func (a *Auth) AuthenticateAndAddIdentityToRequest(c *gin.Context, assertions ...AuthorizationAssertions) (*gin.Context, response.Response, error) {
r := c.Request
ck, err := r.Cookie("access_token")
if err != nil {
return c, response.TemporaryRedirect("/").
JSON("no access_token cookie provided"), nil
}
ctx := r.Context()
accessToken := ck.Value
claims, expiration, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
if err != nil {
if errors.Is(err, consts.ErrNotFound) {
u, err := a.newLoginURL(ctx, a.auth, r.URL.String())
if err != nil {
return c, nil, response.Errorf("failed to generate login url: %w", err)
}
return c, response.TemporaryRedirect(u), nil
}
return c, nil, response.Errorf("failed to authenticate: %w", err)
}
now := time.Now()
// refresh tokens, when the access token is "old enough"
// id token lifetime is 48 hours, allowing a person to use the app everyday comfortably, with wiggle room, without having to log in.
const idTokenLifetime = 48 * time.Hour
if refreshFloor := expiration.Add(-(idTokenLifetime / 4)); refreshFloor.Before(now) {
accessToken, expiration, err = a.auth.RefreshAccessToken(ctx, accessToken)
if err != nil {
a.log.Warn("failed to refresh access token", "error", err)
return c, response.TemporaryRedirect("/").
Body(io.NopCloser(bytes.NewBuffer([]byte(fmt.Sprintf("failed to refresh access token: %v", err))))).
Cookie(cookies.Expired("access_token")), nil
}
// 'redirect' to same url, to set the new access_token cookie
return c, response.TemporaryRedirect(r.URL.String()).
Cookie(cookies.AccessToken(accessToken, expiration)), nil
}
// add identity info to request context
user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken)
if err != nil {
return c, nil, response.Errorf("failed to authorize: %w", err)
}
for _, as := range assertions {
if res, err := as(c); res != nil || err != nil {
return c, res, err
}
}
r = r.WithContext(SetIdentity(ctx, Identity{
AccessToken: accessToken,
Claims: claims,
User: user,
Account: acct,
}))
c.Request = r
return c, nil, nil
}
type identityKey struct{}
// stores identity in request context
+5 -2
View File
@@ -7,15 +7,18 @@ import (
"ruben/inventory2/internal/logging"
"ruben/inventory2/internal/server/response"
"github.com/gin-gonic/gin"
)
func LogRequests(ctx context.Context, logger *logging.Logger) response.Middleware {
reqIDCh := newRequestIDProvider(ctx)
return func(fn response.HandlerFunc) response.HandlerFunc {
return func(r *http.Request) (response.Response, error) {
return func(c *gin.Context) (response.Response, error) {
start := time.Now()
r := c.Request
args := []any{
"id", <-reqIDCh,
"url", r.URL,
@@ -24,7 +27,7 @@ func LogRequests(ctx context.Context, logger *logging.Logger) response.Middlewar
logger.Debug("Request", args...)
res, err := fn(r)
res, err := fn(c)
end := time.Now()
var status int
+7 -7
View File
@@ -1,22 +1,22 @@
package response
import (
"net/http"
"github.com/gin-gonic/gin"
)
type (
HandlerFunc = func(r *http.Request) (Response, error)
HandlerFunc = func(c *gin.Context) (Response, error)
Middleware = func(HandlerFunc) HandlerFunc
)
func Handler(f HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res, err := f(r)
func Handler(f HandlerFunc) gin.HandlerFunc {
return func(c *gin.Context) {
res, err := f(c)
if err != nil {
WriteError(w, err)
WriteError(c, err)
} else {
Write(w, r, res)
Write(c, res)
}
}
}
+8 -4
View File
@@ -4,9 +4,13 @@ import (
"fmt"
"io"
"net/http"
"github.com/gin-gonic/gin"
)
func Write(w http.ResponseWriter, r *http.Request, res Response) {
func Write(c *gin.Context, res Response) {
w := c.Writer
// w.Header() must be set before ResponseWriter.WriteHeader is called
// or redirect is attempted
hdrs := w.Header()
@@ -19,7 +23,7 @@ func Write(w http.ResponseWriter, r *http.Request, res Response) {
}
if code, to, ok := res.GetRedirect(); ok {
http.Redirect(w, r, to, code.Int())
http.Redirect(w, c.Request, to, code.Int())
return
}
@@ -47,8 +51,8 @@ func Write(w http.ResponseWriter, r *http.Request, res Response) {
}
func WriteError(w http.ResponseWriter, err error) {
http.Error(w, err.Error(), GetStatusFromError(err))
func WriteError(c *gin.Context, err error) {
c.String(GetStatusFromError(err), err.Error())
}
func GetStatusFromError(err error) int {
-659
View File
@@ -1,659 +0,0 @@
package router
import (
"fmt"
"net/http"
"path"
"slices"
"strings"
"ruben/inventory2/internal/consts"
"ruben/inventory2/internal/logging"
"ruben/inventory2/internal/server/response"
)
type (
Mux struct {
Mux *http.ServeMux
middleware []response.Middleware
log *logging.Logger
}
)
var (
ErrHandlerNotFound = fmt.Errorf("%w: handler not found", consts.ErrNotFound)
)
func NewMux(log *logging.Logger, ms ...response.Middleware) *Mux {
return &Mux{
Mux: http.NewServeMux(),
middleware: ms,
log: log,
}
}
func (m *Mux) AddMiddleware(ms ...response.Middleware) *Mux {
m.middleware = append(m.middleware, ms...)
return m
}
func (m *Mux) Handle(pattern string, fn response.HandlerFunc) {
log := m.log.With(
"method", "Handle",
"patter", pattern,
"fn", fn,
)
defer log.DebugCallf("called")()
m.Mux.Handle(pattern, response.Handler(m.applyMiddleware(fn)))
}
func (m *Mux) applyMiddleware(fn response.HandlerFunc) response.HandlerFunc {
return applyMiddleware(fn, m.middleware...)
}
func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
m.Mux.ServeHTTP(w, r)
}
// TODO: support for methods?
// TODO: strip the prefix from the call to the subrouter handler call
// Route does not accept methods or ... wildcards
// func (m *Mux) Route(basePathPattern string, sr Subrouter) {
func (m *Mux) Route(pattern string, sr Subrouter) {
log := m.log.With("method", "Route", "pattern", pattern, "sr", sr)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", nil
})
// ---
method, segments, _ := getHTTPMethodAndPathSegments(m.log, pattern)
if pattern == "" || (len(segments) == 0 && pattern[len(pattern)-1] != '/') {
panic("invalid subpath: " + pattern)
}
numPatternSegments := len(segments)
handler := response.Handler(m.applyMiddleware(buildSubrouterHandlerFunc(m.log, numPatternSegments, sr)))
cleanPattern := "/"
if joinedSegments := strings.Join(segments, "/"); joinedSegments != "" {
cleanPattern = "/" + joinedSegments + "/"
}
if method != "" {
cleanPattern = method + " " + cleanPattern
}
log.Debugf("cleanPattern: %s", cleanPattern)
// ---
/*
trimmedSubpathPattern := strings.Trim(path.Clean(basePathPattern), "/")
if trimmedSubpathPattern == "" && basePathPattern != "/" {
panic("invalid subpath: " + basePathPattern)
}
var trimmedPatternSegments []string
if trimmedSubpathPattern != "" {
trimmedPatternSegments = strings.Split(trimmedSubpathPattern, "/")
}
numPatternSegments := len(trimmedPatternSegments)
handler := response.Handler(m.applyMiddleware(buildSubrouterHandlerFunc(numPatternSegments, sr)))
cleanPattern := "/"
if joinedSegments := strings.Join(trimmedPatternSegments, "/"); joinedSegments != "" {
cleanPattern = "/" + joinedSegments + "/"
}
fmt.Println("Mux.Route: cleanPattern:", cleanPattern)
*/
if method != "" {
m.Mux.Handle(method+" "+cleanPattern, handler)
} else {
m.Mux.Handle("GET "+cleanPattern, handler)
m.Mux.Handle("POST "+cleanPattern, handler)
m.Mux.Handle("PUT "+cleanPattern, handler)
m.Mux.Handle("PATCH "+cleanPattern, handler)
m.Mux.Handle("DELETE "+cleanPattern, handler)
}
}
type (
Subrouter interface {
Handler(r *http.Request) (fn response.HandlerFunc, pathParams map[string]string, found bool)
}
SubMux struct {
tree *muxTree
middleware []response.Middleware
log *logging.Logger
}
)
func NewSubMux(log *logging.Logger, ms ...response.Middleware) *SubMux {
return &SubMux{
tree: newMuxTree(log.WithGroup("muxTree")),
middleware: ms,
log: log,
}
}
// TODO: this is capturing all subroutes!
func (m *SubMux) Handle(pattern string, fn response.HandlerFunc) {
defer m.log.With("pattern", pattern, "fn", fn).DebugCallf("Handle")()
m.tree.set(pattern, m.applyMiddleware(fn))
}
// TODO: need http method support
// Route does not accept methods or ... wildcards
// func (m *SubMux) Route(basePathPattern string, sr Subrouter) {
func (m *SubMux) Route(pattern string, sr Subrouter) {
log := m.log.With("method", "Route", "pattern", pattern, "sr", sr)
defer log.DebugCallf("Route")()
method, segments, _ := getHTTPMethodAndPathSegments(m.log, pattern)
if pattern == "" || (len(segments) == 0 && pattern[len(pattern)-1] != '/') {
panic("invalid subpath: " + pattern)
}
numPatternSegments := len(segments)
handler := m.applyMiddleware(buildSubrouterHandlerFunc(m.log, numPatternSegments, sr))
cleanPattern := "/"
if joinedSegments := strings.Join(segments, "/"); joinedSegments != "" {
cleanPattern = "/" + joinedSegments + "/"
}
cleanPattern = path.Clean(cleanPattern)
if method != "" {
cleanPattern = method + " " + cleanPattern
}
log.Debugf("Handle about to be called: cleanPattern: %s", cleanPattern)
// ---
/*
// ---
trimmedSubpathPattern := strings.Trim(path.Clean(basePathPattern), "/")
if trimmedSubpathPattern == "" && basePathPattern != "/" {
panic("invalid subpath: " + basePathPattern)
}
var trimmedPatternSegments []string
if trimmedSubpathPattern != "" {
trimmedPatternSegments = strings.Split(trimmedSubpathPattern, "/")
}
numPatternSegments := len(trimmedPatternSegments)
handler := m.applyMiddleware(buildSubrouterHandlerFunc(m.log, numPatternSegments, sr))
// TODO: we're wrapping the pattern method!
cleanPattern := "/"
if joinedSegments := strings.Join(trimmedPatternSegments, "/"); joinedSegments != "" {
cleanPattern = "/" + joinedSegments + "/"
}
// ---
*/
m.Handle(cleanPattern, handler)
/*
m.Mux.Handle("GET "+cleanPattern, handler)
m.Mux.Handle("POST "+cleanPattern, handler)
m.Mux.Handle("PUT "+cleanPattern, handler)
m.Mux.Handle("PATCH "+cleanPattern, handler)
m.Mux.Handle("DELETE "+cleanPattern, handler)
*/
}
// // Route does not accept methods or ... wildcards
// func (m *SubMux) Route(basePathPattern string, sr Subrouter) {
// trimmedSubpathPattern := strings.Trim(path.Clean(basePathPattern), "/")
// if trimmedSubpathPattern == "" && basePathPattern != "/" {
// panic("invalid subpath: " + basePathPattern)
// }
//
// var trimmedPatternSegments []string
// if trimmedSubpathPattern != "" {
// trimmedPatternSegments = strings.Split(trimmedSubpathPattern, "/")
// }
// numPatternSegments := len(trimmedPatternSegments)
//
// handler := m.applyMiddleware(buildSubrouterHandlerFunc(numPatternSegments, sr))
//
// // TODO: we're wrapping the pattern method!
// cleanPattern := "/"
// if joinedSegments := strings.Join(trimmedPatternSegments, "/"); joinedSegments != "" {
// cleanPattern = "/" + joinedSegments + "/"
// }
//
// m.Handle(cleanPattern, handler)
//
// /*
// m.Mux.Handle("GET "+cleanPattern, handler)
// m.Mux.Handle("POST "+cleanPattern, handler)
// m.Mux.Handle("PUT "+cleanPattern, handler)
// m.Mux.Handle("PATCH "+cleanPattern, handler)
// m.Mux.Handle("DELETE "+cleanPattern, handler)
// */
// }
func buildSubrouterHandlerFunc(logger *logging.Logger, numPatternSegments int, sr Subrouter) response.HandlerFunc {
log := logger.With(
"function", "buildSubrouterHandlerFunc",
"numPatternSegments", numPatternSegments,
)
return func(r *http.Request) (res response.Response, err error) {
log := log.With("r.URL", r.URL)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"res", res,
"err", err,
}
})
fullPath := r.URL.Path
// set trailing path on request
segments, trailingSlash := getPathSegments(logger, r.URL.Path)
/*
endsInSlash := strings.HasSuffix(r.URL.Path, "/")
segments := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
*/
tail := segments[numPatternSegments:]
tailStr := "/"
if len(tail) > 0 {
tailStr += strings.Join(tail, "/")
if trailingSlash {
tailStr += "/"
}
}
r.URL.Path = tailStr
defer func() {
// reset the request path
r.URL.Path = fullPath
}()
// do the request
log.Debugf("handler called: %s", r.URL)
fn, params, ok := sr.Handler(r)
log.Debugf("handler returned: url = %s, fn = %v, params = %v, ok = %v", r.URL, fn, params, ok)
if !ok {
return nil, response.NotFound().Wrap(ErrHandlerNotFound)
}
for k, v := range params {
r.SetPathValue(k, v)
}
res, err = fn(r)
return res, err
}
}
func (m *SubMux) Handler(r *http.Request) (fn response.HandlerFunc, params map[string]string, found bool) {
log := m.log.With(
"method", "Handler",
"r.URL", r.URL,
)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"fn", fn,
"params", params,
"found", found,
}
})
return m.tree.get(r.Method, r.URL.Path)
}
func (m *SubMux) applyMiddleware(fn response.HandlerFunc) response.HandlerFunc {
return applyMiddleware(fn, m.middleware...)
}
// path pattern matching tree implementation
type (
muxTree struct {
log *logging.Logger
branches map[string]*muxTree
wildcardKey string
wildcardBranch *muxTree
method string
handler response.HandlerFunc
subrouteHandler response.HandlerFunc
}
)
func newMuxTree(log *logging.Logger) *muxTree {
return &muxTree{
log: log,
branches: make(map[string]*muxTree),
}
}
func (m *muxTree) String() string {
if m == nil {
return "<nil>"
}
kvs := make([]string, 0, 6)
if m.subrouteHandler != nil {
kvs = append(kvs, fmt.Sprintf("subrouteHandler: %v", m.subrouteHandler))
}
if m.method != "" {
kvs = append(kvs, fmt.Sprintf("method: %v", m.method))
}
if m.handler != nil {
kvs = append(kvs, fmt.Sprintf("handler: %v", m.handler))
}
if m.wildcardKey != "" {
kvs = append(kvs, fmt.Sprintf("wildcardKey: %v", m.wildcardKey))
}
if m.wildcardBranch != nil {
kvs = append(kvs, fmt.Sprintf("wildcardBranch: %v", m.wildcardBranch))
}
if len(m.branches) > 0 {
branchKvs := make([]string, 0, len(m.branches))
for k, v := range m.branches {
branchKvs = append(branchKvs, fmt.Sprintf("%q: %s", k, v))
}
slices.Sort(branchKvs)
kvs = append(kvs, fmt.Sprintf("branches: {%s}", strings.Join(branchKvs, ", ")))
}
return fmt.Sprintf("{%s}", strings.Join(kvs, ", "))
}
func (m *muxTree) set(pattern string, fn response.HandlerFunc) {
log := m.log.With(
"method", "set",
"patter", pattern,
"fn", fn,
)
defer log.DebugCallf("called")()
method, segments, trailingSlash, endOfURLWildcard := getHTTPMethodAndPathSegmentsDroppingEndOfURLWildcard(m.log, pattern)
m.setBySegments(
method,
segments,
trailingSlash,
endOfURLWildcard,
fn,
)
}
// TODO: handle /{$} properly
func (m *muxTree) setBySegments(method string, segments []string, trailingSlash, endOfURLWildcard bool, fn response.HandlerFunc) {
log := m.log.With(
"method", "setBySegments",
"arg.method", method,
"segments", segments,
"trailingSlash", trailingSlash,
"endOfURLWildcard", endOfURLWildcard,
"fn", fn,
)
defer log.DebugCallf("called")()
if len(segments) == 0 {
m.method = method
if !trailingSlash || endOfURLWildcard {
m.handler = fn
} else if trailingSlash {
m.subrouteHandler = fn
}
return
}
head := segments[0]
tail := segments[1:]
var sub *muxTree
if key, ok := getWildcardPathSegmentKey(head); ok {
m.wildcardKey = key
if sub = m.wildcardBranch; sub == nil {
sub = newMuxTree(m.log)
m.wildcardBranch = sub
}
} else {
if sub = m.branches[head]; sub == nil {
sub = newMuxTree(m.log)
m.branches[head] = sub
}
}
/*
if isWildcard, wildcardKey := isWildcardPathSegment(head); wildcardKey == "$" {
// TODO: test
// TODO: handle trailing end path matching
if len(tail) > 0 || trailingSlash {
panic("{$} wildcard applied outside of end of path")
}
m.method = method
m.handler = fn
return
} else if isWildcard {
m.wildcardKey = wildcardKey
if sub = m.wildcardBranch; sub == nil {
sub = newMuxTree(m.log)
m.wildcardBranch = sub
}
} else {
if sub = m.branches[head]; sub == nil {
sub = newMuxTree(m.log)
m.branches[head] = sub
}
}
*/
sub.setBySegments(method, tail, trailingSlash, endOfURLWildcard, fn)
}
func (m *muxTree) get(method, pattern string) (fn response.HandlerFunc, params map[string]string, found bool) {
log := m.log.With(
"method", "get",
"arg.method", method,
"pattern", pattern,
)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"fn", fn,
"params", params,
"found", found,
}
})
segments, trailingSlash := getPathSegments(m.log, pattern)
return m.getByHTTPMethodAndSegments(method, segments, trailingSlash)
}
func (m *muxTree) getByHTTPMethodAndSegments(method string, segments []string, trailingSlash bool) (fn response.HandlerFunc, params map[string]string, found bool) {
log := m.log.With(
"method", "getByHTTPMethodAndSegments",
"arg.method", method,
"segments", segments,
"trailingSlash", trailingSlash,
"muxTree", m,
)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"fn", fn,
"params", params,
"found", found,
}
})
if len(segments) == 0 {
log.Debugf("no segments")
if methodMatches := m.method == "" || m.method == method; methodMatches {
if fn = m.handler; fn == nil {
fn = m.subrouteHandler
}
return fn, map[string]string{}, fn != nil
}
return nil, map[string]string{}, false
}
head := segments[0]
tail := segments[1:]
if m.subrouteHandler != nil {
defer func() {
if !found && (m.method == "" || m.method == method) {
fn = m.subrouteHandler
found = true
}
}()
}
if sm, ok := m.branches[head]; ok {
log.Debugf("matching branch found")
//return sm.getByHTTPMethodAndSegments(method, tail, trailingSlash)
if fn, params, found = sm.getByHTTPMethodAndSegments(method, tail, trailingSlash); found {
return fn, params, found
}
log.Debugf("matching branch mismatched at subpath")
} else {
log.Debugf("no matching branch found")
}
if sm := m.wildcardBranch; sm != nil {
wlog := log.With(
"wildcardKey", m.wildcardKey,
"sub.muxTree", sm,
)
wlog.Debugf("wildcard branch found")
//fn, params, found = sm.getByHTTPMethodAndSegments(method, tail, trailingSlash)
if fn, params, found = sm.getByHTTPMethodAndSegments(method, tail, trailingSlash); found {
params[m.wildcardKey] = head
return fn, params, found
}
wlog.Debugf("wildcard branch mismatched at subpath")
} else {
log.Debugf("no wildcard branch found")
}
// --- TODO: test ---
if m.subrouteHandler != nil && (m.method == method || m.method == "") {
log.Debugf("subroutes captured")
return m.subrouteHandler, nil, true
}
log.Debugf("subroutes not captured")
// ---
return nil, map[string]string{}, false
}
func getHTTPMethodAndPathSegmentsDroppingEndOfURLWildcard(logger *logging.Logger, pattern string) (method string, segments []string, trailingSlash, endOfURLWildcard bool) {
log := logger.With(
"function", "getHTTPMethodAndPathSegmentsDroppingEndOfURLWildcard",
"pattern", pattern,
)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"method", method,
"segments", segments,
"trailingSlash", trailingSlash,
"endOfURLWildcard", endOfURLWildcard,
}
})
if vs := strings.SplitN(pattern, " ", 2); len(vs) == 2 {
method = vs[0]
pattern = vs[1]
}
segments, trailingSlash, endOfURLWildcard = getPathSegmentsWithoutEndOfURLWildcard(logger, pattern)
return method, segments, trailingSlash, endOfURLWildcard
}
func getHTTPMethodAndPathSegments(logger *logging.Logger, pattern string) (method string, segments []string, trailingSlash bool) {
log := logger.With(
"function", "getHTTPMethodAndPathSegments",
"pattern", pattern,
)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"method", method,
"segments", segments,
"trailingSlash", trailingSlash,
}
})
if vs := strings.SplitN(pattern, " ", 2); len(vs) == 2 {
method = vs[0]
pattern = vs[1]
}
segments, trailingSlash = getPathSegments(logger, pattern)
return method, segments, trailingSlash
}
func getPathSegments(logger *logging.Logger, pathPattern string) (segments []string, trailingSlash bool) {
log := logger.With(
"function", "getPathSegments",
"pathPattern", pathPattern,
)
defer log.DebugCallf("getPathSegments")()
trailingSlash = strings.HasSuffix(pathPattern, "/")
p := strings.Trim(path.Clean(pathPattern), "/")
if p == "" {
return nil, trailingSlash
}
return strings.Split(p, "/"), trailingSlash
}
// NOTE: if endOfURLWildcard, then trailingSlash
func getPathSegmentsWithoutEndOfURLWildcard(logger *logging.Logger, pathPattern string) (segments []string, trailingSlash, endOfURLWildcard bool) {
log := logger.With(
"function", "getPathSegmentsWithoutEndOfURLWildcard",
"pathPattern", pathPattern,
)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"segments", segments,
"trailingSlash", trailingSlash,
"endOfURLWildcard", endOfURLWildcard,
}
})
endOfURLWildcard = strings.HasSuffix(pathPattern, "/{$}")
if endOfURLWildcard {
pathPattern = pathPattern[:len(pathPattern)-3]
}
trailingSlash = strings.HasSuffix(pathPattern, "/")
p := strings.Trim(path.Clean(pathPattern), "/")
if p == "" {
return nil, trailingSlash, endOfURLWildcard
}
return strings.Split(p, "/"), trailingSlash, endOfURLWildcard
}
func getWildcardPathSegmentKey(s string) (string, bool) {
if isWildcard := len(s) > 2 && s[0] == '{' && s[len(s)-1] == '}'; isWildcard {
return s[1 : len(s)-1], true
}
return "", false
}
func applyMiddleware(fn response.HandlerFunc, ms ...response.Middleware) response.HandlerFunc {
for _, mw := range slices.Backward(ms) {
prev := fn
fn = mw(func(r *http.Request) (response.Response, error) {
return prev(r)
})
}
return fn
}
// TODO: remove the consts. trace level
+49 -49
View File
@@ -3,14 +3,15 @@ package server
import (
"context"
"encoding/json"
"errors"
"fmt"
"html/template"
"net/http"
"path"
"strconv"
"strings"
"github.com/angelbeltran/templater"
"github.com/gin-gonic/gin"
"ruben/inventory2/internal/domains/accounts"
"ruben/inventory2/internal/domains/authentication"
@@ -23,11 +24,11 @@ import (
"ruben/inventory2/internal/server/api/webhooks"
etsy_webhooks "ruben/inventory2/internal/server/api/webhooks/etsy"
"ruben/inventory2/internal/server/middleware"
"ruben/inventory2/internal/server/response"
"ruben/inventory2/internal/server/router"
)
func NewServer(
// r gin.IRouter, // TODO: use this everywhere
func Router(
ctx context.Context,
logger *logging.Logger,
contentDir string,
@@ -35,11 +36,8 @@ func NewServer(
accts *accounts.Store,
etsy *etsy_platform.Platform,
auth *authentication.Authenticator,
) http.Handler {
mux := router.NewMux(
logger,
middleware.LogRequests(ctx, logger.WithGroup("request")),
)
) *gin.Engine {
r := gin.Default()
// TODO: shouldn't this ACTUALLY be a middleware?
// - only try to make this an actual middleware AFTER all the routers are broken out, so that way how the middleware is supposed to work can be known
@@ -54,22 +52,26 @@ func NewServer(
// non-html content: scripts, styles, images, etc
scfs := http.FileServer(http.Dir(contentDir + "/scripts"))
mux.Mux.Handle("GET /scripts/", http.StripPrefix("/scripts", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Use(fileServer("/scripts", contentDir+"/scripts", func(c *gin.Context) {
w := c.Writer
w.Header().Set("Content-Type", "text/javascript")
if path.Ext(r.URL.Path) == ".gz" {
if path.Ext(c.Request.URL.Path) == ".gz" {
w.Header().Set("Content-Encoding", "gzip")
}
scfs.ServeHTTP(w, r)
})))
mux.Mux.Handle("GET /styles/", http.StripPrefix("/styles", http.FileServer(http.Dir(contentDir+"/styles"))))
mux.Mux.Handle("GET /favicon/", http.StripPrefix("/favicon", http.FileServer(http.Dir(contentDir+"/favicon"))))
}))
r.Static("/styles", "./styles")
r.Static("/favicon", "./favicon")
// html (must be before the api endpoints, because the webpage middleware has to be installed beforehand
r.GET("/", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/ui")
})
// kind of a dumb way to capture routes for webpages
wpr := templates_api.NewWebpageRouter(
templates_api.SetupRoutes(
logger.WithGroup("templates"),
r.Group("/ui"),
contentDir,
templater.NewTemplater(
contentDir+"/templates",
@@ -125,42 +127,27 @@ func NewServer(
etsy,
authMiddleware,
)
mux.AddMiddleware(
func(fn response.HandlerFunc) response.HandlerFunc {
return func(r *http.Request) (response.Response, error) {
res, err := fn(r)
if err == nil || !errors.Is(err, router.ErrHandlerNotFound) {
return res, err
}
renderWebPage, pathParams, found := wpr.Handler(r)
if !found {
return nil, response.NotFound()
}
for k, v := range pathParams {
r.SetPathValue(k, v)
}
return renderWebPage(r)
}
},
)
mux.Route("/", wpr)
// api endpoints
mux.Route("/auth", auth_api.NewLoginSubrouter(logger.WithGroup("/auth"), auth))
mux.Route("/accounts", accounts_api.NewAccountSubrouter(
api := r.Group("/api")
auth_api.Routes(
api.Group("/auth"),
logger.WithGroup("/auth"),
auth,
)
accounts_api.Routes(
api.Group("/accounts"),
logger.WithGroup("/accounts"),
accts,
authMiddleware,
))
)
// api webhooks (TODO: make a router for these)
webhookHandler := http.StripPrefix("/webhooks", webhooks.New(
webhooks.Webhooks(
api.Group("/webhooks"),
logger.WithGroup("webhooks"),
rawEvents,
etsy,
@@ -169,10 +156,23 @@ func NewServer(
OAuthRedirectURIWithAcctIDParam: "/oauth/account/{acctID}/auth_code",
},
},
))
mux.Mux.Handle("GET /webhooks/", webhookHandler)
mux.Mux.Handle("POST /webhooks/", webhookHandler)
mux.Mux.Handle("PUT /webhooks/", webhookHandler)
)
return mux
return r
}
func fileServer(urlPrefix, dir string, beforeServe func(c *gin.Context)) gin.HandlerFunc {
scfs := http.StripPrefix(urlPrefix, http.FileServer(http.Dir(dir)))
return func(c *gin.Context) {
r := c.Request
if r.URL.Path == urlPrefix || strings.HasPrefix(r.URL.Path, path.Join(urlPrefix, "/")) {
if beforeServe != nil {
beforeServe(c)
}
scfs.ServeHTTP(c.Writer, r)
c.Abort()
}
// TODO: need a c.Next()?
}
}