subrouters
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
This favicon was generated using the following graphics from Twitter Twemoji:
|
||||||
|
|
||||||
|
- Graphics Title: 2699.svg
|
||||||
|
- Graphics Author: Copyright 2020 Twitter, Inc and other contributors (https://github.com/twitter/twemoji)
|
||||||
|
- Graphics Source: https://github.com/twitter/twemoji/blob/master/assets/svg/2699.svg
|
||||||
|
- Graphics License: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 507 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
|||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package consts
|
||||||
|
|
||||||
|
import "log/slog"
|
||||||
|
|
||||||
|
const (
|
||||||
|
LevelTrace slog.Level = slog.LevelDebug - 4
|
||||||
|
)
|
||||||
@@ -31,7 +31,7 @@ const (
|
|||||||
AUTH0_CLIENT_SECRET = "83U-iWdVaNnwk9XDzteo_2VMyOq_l1siKYqg1_2E7jCzgL8MnkaxlysPMcPMGlxA"
|
AUTH0_CLIENT_SECRET = "83U-iWdVaNnwk9XDzteo_2VMyOq_l1siKYqg1_2E7jCzgL8MnkaxlysPMcPMGlxA"
|
||||||
|
|
||||||
// The Callback URL of our application.
|
// The Callback URL of our application.
|
||||||
AUTH0_CALLBACK_URL = "https://inventory-plus-plus.com/login/callback"
|
AUTH0_CALLBACK_URL = "https://inventory-plus-plus.com/auth/login/callback"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|||||||
@@ -1,19 +1,52 @@
|
|||||||
package server
|
package accounts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"ruben/inventory2/internal/consts"
|
"ruben/inventory2/internal/consts"
|
||||||
"ruben/inventory2/internal/domains/accounts"
|
"ruben/inventory2/internal/domains/accounts"
|
||||||
"ruben/inventory2/internal/server/middleware"
|
"ruben/inventory2/internal/server/middleware"
|
||||||
"ruben/inventory2/internal/server/response"
|
"ruben/inventory2/internal/server/response"
|
||||||
|
"ruben/inventory2/internal/server/router"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type accountSubrouter struct {
|
||||||
|
log *slog.Logger
|
||||||
|
*router.SubMux
|
||||||
|
accts *accounts.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccountSubrouter(
|
||||||
|
logger *slog.Logger,
|
||||||
|
accts *accounts.Store,
|
||||||
|
authMiddleware *middleware.Auth,
|
||||||
|
) *accountSubrouter {
|
||||||
|
mux := router.NewSubMux(logger)
|
||||||
|
|
||||||
|
as := &accountSubrouter{
|
||||||
|
log: logger,
|
||||||
|
SubMux: mux,
|
||||||
|
accts: accts,
|
||||||
|
}
|
||||||
|
|
||||||
|
withAuth := func(fn response.HandlerFunc) response.HandlerFunc {
|
||||||
|
return 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
|
||||||
|
}
|
||||||
|
|
||||||
// POST /accounts
|
// POST /accounts
|
||||||
func (s *Server) createAccount(r *http.Request) (response.Response, error) {
|
func (s *accountSubrouter) createAccount(r *http.Request) (response.Response, error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
email := r.FormValue("email")
|
email := r.FormValue("email")
|
||||||
if email == "" {
|
if email == "" {
|
||||||
@@ -35,7 +68,7 @@ func (s *Server) createAccount(r *http.Request) (response.Response, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// POST /accounts/{acctID}/inventory/sync-groups/draft/listings
|
// POST /accounts/{acctID}/inventory/sync-groups/draft/listings
|
||||||
func (s *Server) createSyncGroupListingDraft(r *http.Request) (response.Response, error) {
|
func (s *accountSubrouter) createSyncGroupListingDraft(r *http.Request) (response.Response, error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
acctID := middleware.GetIdentity(ctx).Account.AccountID
|
acctID := middleware.GetIdentity(ctx).Account.AccountID
|
||||||
|
|
||||||
@@ -53,7 +86,7 @@ func (s *Server) createSyncGroupListingDraft(r *http.Request) (response.Response
|
|||||||
// PUT /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/shop
|
// PUT /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/shop
|
||||||
// @platform string
|
// @platform string
|
||||||
// @shopID string
|
// @shopID string
|
||||||
func (s *Server) setShopInSyncGroupListingDraft(r *http.Request) (response.Response, error) {
|
func (s *accountSubrouter) setShopInSyncGroupListingDraft(r *http.Request) (response.Response, error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
acctID := middleware.GetIdentity(ctx).Account.AccountID
|
acctID := middleware.GetIdentity(ctx).Account.AccountID
|
||||||
|
|
||||||
@@ -76,7 +109,7 @@ func (s *Server) setShopInSyncGroupListingDraft(r *http.Request) (response.Respo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.accts.SetShopInSyncGroupListingDraft(ctx, acctID, orderIndex, platform, shopID); err != nil {
|
if err := s.accts.SetShopInSyncGroupListingDraft(ctx, acctID, orderIndex, platform, shopID); err != nil {
|
||||||
return nil, response.Errorf("failed to set shop: %w", mapConstantErrorsToHTTPErrors(err))
|
return nil, response.Errorf("failed to set shop: %w", response.ErrorFromConstant(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Redirect(
|
return response.Redirect(
|
||||||
@@ -86,7 +119,7 @@ func (s *Server) setShopInSyncGroupListingDraft(r *http.Request) (response.Respo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PUT /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/listing
|
// PUT /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/listing
|
||||||
func (s *Server) setListingInSyncGroupListingDraft(r *http.Request) (response.Response, error) {
|
func (s *accountSubrouter) setListingInSyncGroupListingDraft(r *http.Request) (response.Response, error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
acctID := middleware.GetIdentity(ctx).Account.AccountID
|
acctID := middleware.GetIdentity(ctx).Account.AccountID
|
||||||
|
|
||||||
@@ -102,7 +135,7 @@ func (s *Server) setListingInSyncGroupListingDraft(r *http.Request) (response.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.accts.SetListingInSyncGroupListingDraft(ctx, acctID, orderIndex, listingID); err != nil {
|
if err := s.accts.SetListingInSyncGroupListingDraft(ctx, acctID, orderIndex, listingID); err != nil {
|
||||||
return nil, response.Errorf("failed to set listing: %w", mapConstantErrorsToHTTPErrors(err))
|
return nil, response.Errorf("failed to set listing: %w", response.ErrorFromConstant(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Redirect(
|
return response.Redirect(
|
||||||
@@ -112,7 +145,7 @@ func (s *Server) setListingInSyncGroupListingDraft(r *http.Request) (response.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DELETE /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}
|
// DELETE /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}
|
||||||
func (s *Server) deleteSyncGroupListingDraft(r *http.Request) (response.Response, error) {
|
func (s *accountSubrouter) deleteSyncGroupListingDraft(r *http.Request) (response.Response, error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
acctID := middleware.GetIdentity(ctx).Account.AccountID
|
acctID := middleware.GetIdentity(ctx).Account.AccountID
|
||||||
|
|
||||||
@@ -122,20 +155,20 @@ func (s *Server) deleteSyncGroupListingDraft(r *http.Request) (response.Response
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.accts.DeleteSyncGroupListingDraft(ctx, acctID, orderIndex); err != nil {
|
if _, err := s.accts.DeleteSyncGroupListingDraft(ctx, acctID, orderIndex); err != nil {
|
||||||
return nil, response.Errorf("failed to delete listing: %w", mapConstantErrorsToHTTPErrors(err))
|
return nil, response.Errorf("failed to delete listing: %w", response.ErrorFromConstant(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Status(200), nil
|
return response.Status(200), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /accounts/{acctID}/inventory/sync-groups
|
// POST /accounts/{acctID}/inventory/sync-groups
|
||||||
func (s *Server) saveNewSyncGroup(r *http.Request) (response.Response, error) {
|
func (s *accountSubrouter) saveNewSyncGroup(r *http.Request) (response.Response, error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
acctID := middleware.GetIdentity(ctx).Account.AccountID
|
acctID := middleware.GetIdentity(ctx).Account.AccountID
|
||||||
|
|
||||||
grp, err := s.accts.SaveNewSyncGroup(ctx, acctID)
|
grp, err := s.accts.SaveNewSyncGroup(ctx, acctID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, response.Errorf("failed to save new sync group: %w", mapConstantErrorsToHTTPErrors(err))
|
return nil, response.Errorf("failed to save new sync group: %w", response.ErrorFromConstant(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Redirect(
|
return response.Redirect(
|
||||||
@@ -147,10 +180,15 @@ func (s *Server) saveNewSyncGroup(r *http.Request) (response.Response, error) {
|
|||||||
|
|
||||||
func getOrderIndexForSyncGroupListingDraftFromPath(r *http.Request) (int, error) {
|
func getOrderIndexForSyncGroupListingDraftFromPath(r *http.Request) (int, error) {
|
||||||
orderIndexStr := r.PathValue("orderIndex")
|
orderIndexStr := r.PathValue("orderIndex")
|
||||||
|
if orderIndexStr == "" {
|
||||||
|
return 0, response.NotFound().
|
||||||
|
Msg("no orderIndex found")
|
||||||
|
}
|
||||||
|
|
||||||
orderIndex, err := strconv.Atoi(orderIndexStr)
|
orderIndex, err := strconv.Atoi(orderIndexStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, response.NotFound().
|
return 0, response.NotFound().
|
||||||
Msgf("no listing draft found at %s", orderIndexStr)
|
Msgf("invalid order index: %s", orderIndexStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return orderIndex, nil
|
return orderIndex, nil
|
||||||
@@ -1,21 +1,58 @@
|
|||||||
package server
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"ruben/inventory2/internal/domains/authentication"
|
"ruben/inventory2/internal/domains/authentication"
|
||||||
"ruben/inventory2/internal/server/cookies"
|
"ruben/inventory2/internal/server/cookies"
|
||||||
"ruben/inventory2/internal/server/response"
|
"ruben/inventory2/internal/server/response"
|
||||||
|
"ruben/inventory2/internal/server/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: use a login/logout server? (prefix: '/auth'?)
|
type loginSubrouter struct {
|
||||||
|
log *slog.Logger
|
||||||
|
auth *authentication.Authenticator
|
||||||
|
router.Subrouter
|
||||||
|
}
|
||||||
|
|
||||||
// GET /login
|
func NewLoginSubrouter(logger *slog.Logger, auth *authentication.Authenticator) *loginSubrouter {
|
||||||
func (s *Server) loginPage(r *http.Request) (response.Response, error) {
|
mux := router.NewSubMux(logger)
|
||||||
|
|
||||||
|
ls := &loginSubrouter{
|
||||||
|
log: logger,
|
||||||
|
auth: 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) newLoginSubrouter(logger *slog.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) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
u, err := newLoginURL(ctx, s.auth, "/")
|
u, err := NewLoginURL(ctx, s.auth, "/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -23,7 +60,7 @@ func (s *Server) loginPage(r *http.Request) (response.Response, error) {
|
|||||||
return response.TemporaryRedirect(u), nil
|
return response.TemporaryRedirect(u), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLoginURL(ctx context.Context, auth *authentication.Authenticator, targetURI string) (string, error) {
|
func NewLoginURL(ctx context.Context, auth *authentication.Authenticator, targetURI string) (string, error) {
|
||||||
state, err := auth.NewState(ctx, targetURI)
|
state, err := auth.NewState(ctx, targetURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to generate random state: %w", err)
|
return "", fmt.Errorf("failed to generate random state: %w", err)
|
||||||
@@ -34,8 +71,7 @@ func newLoginURL(ctx context.Context, auth *authentication.Authenticator, target
|
|||||||
return auth.AuthCodeURL(base64EncodedState), nil
|
return auth.AuthCodeURL(base64EncodedState), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /login/callback
|
func (s *loginSubrouter) loginCallback(r *http.Request) (response.Response, error) {
|
||||||
func (s *Server) loginCallback(r *http.Request) (response.Response, error) {
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
|
|
||||||
@@ -54,8 +90,7 @@ func (s *Server) loginCallback(r *http.Request) (response.Response, error) {
|
|||||||
Cookie(cookies.AccessToken(accessToken, expiration)), nil
|
Cookie(cookies.AccessToken(accessToken, expiration)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /logout
|
func (s *loginSubrouter) logoutPage(r *http.Request) (response.Response, error) {
|
||||||
func (s *Server) logoutPage(r *http.Request) (response.Response, error) {
|
|
||||||
host := r.Header.Get("X-Forwarded-Host")
|
host := r.Header.Get("X-Forwarded-Host")
|
||||||
if host == "" {
|
if host == "" {
|
||||||
host = r.Host
|
host = r.Host
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package server
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -11,13 +12,62 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"ruben/inventory2/internal/domains/accounts"
|
||||||
|
etsy_platform "ruben/inventory2/internal/domains/platforms/etsy"
|
||||||
|
"ruben/inventory2/internal/domains/raw_events"
|
||||||
"ruben/inventory2/internal/server/middleware"
|
"ruben/inventory2/internal/server/middleware"
|
||||||
"ruben/inventory2/internal/server/response"
|
"ruben/inventory2/internal/server/response"
|
||||||
|
"ruben/inventory2/internal/server/router"
|
||||||
|
|
||||||
|
"github.com/angelbeltran/templater"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type webpageRouter struct {
|
||||||
|
log *slog.Logger
|
||||||
|
contentDir string
|
||||||
|
templater *templater.Templater
|
||||||
|
rawEvents *raw_events.Store
|
||||||
|
accts *accounts.Store
|
||||||
|
etsy *etsy_platform.Platform
|
||||||
|
router.Subrouter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebpageRouter(
|
||||||
|
logger *slog.Logger,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// non-authenticated
|
||||||
|
mux.Handle("GET /{$}", authMiddleware.AddIdentity(wr.serveTemplates))
|
||||||
|
|
||||||
|
// authenticated
|
||||||
|
mux.Handle("GET /", authMiddleware.AuthenticateAndAddIdentity(wr.serveTemplates))
|
||||||
|
|
||||||
|
return wr
|
||||||
|
}
|
||||||
|
|
||||||
// GET /
|
// GET /
|
||||||
// compiles the page template or component template matching the url
|
// compiles the page template or component template matching the url
|
||||||
func (s *Server) serveTemplates(r *http.Request) (response.Response, error) {
|
// func (s *Server) serveTemplates(r *http.Request) (response.Response, error) {
|
||||||
|
func (s *webpageRouter) serveTemplates(r *http.Request) (response.Response, error) {
|
||||||
|
fmt.Println("webpageRouter.serveTemplates:", r.URL)
|
||||||
|
|
||||||
name, args := s.getTemplateNameAndArgs(r, s.contentDir+"/templates/component_bodies")
|
name, args := s.getTemplateNameAndArgs(r, s.contentDir+"/templates/component_bodies")
|
||||||
|
|
||||||
b, err := s.templater.ExecuteComponentBody(name, args...)
|
b, err := s.templater.ExecuteComponentBody(name, args...)
|
||||||
@@ -38,7 +88,9 @@ func (s *Server) serveTemplates(r *http.Request) (response.Response, error) {
|
|||||||
return s.handleTemplateError(err, args...)
|
return s.handleTemplateError(err, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getTemplateNameAndArgs(r *http.Request, templateDir string) (name string, args []any) {
|
// func (s *Server) getTemplateNameAndArgs(r *http.Request, templateDir string) (name string, args []any) {
|
||||||
|
func (s *webpageRouter) getTemplateNameAndArgs(r *http.Request, templateDir string) (name string, args []any) {
|
||||||
|
fmt.Println("webpageRouter.getTemplateNameAndArgs:", r.URL)
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
name, pathParams := getTemplateNameForURL(r.URL, templateDir)
|
name, pathParams := getTemplateNameForURL(r.URL, templateDir)
|
||||||
|
|
||||||
@@ -137,7 +189,8 @@ func getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(file
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleTemplateError(err error, templateArgs ...any) (response.Response, error) {
|
// func (s *Server) handleTemplateError(err error, templateArgs ...any) (response.Response, error) {
|
||||||
|
func (s *webpageRouter) handleTemplateError(err error, templateArgs ...any) (response.Response, error) {
|
||||||
if isFileNotFoundError(err) {
|
if isFileNotFoundError(err) {
|
||||||
return nil, response.NotFound().
|
return nil, response.NotFound().
|
||||||
Wrap(err).
|
Wrap(err).
|
||||||
@@ -19,6 +19,7 @@ func LogRequests(ctx context.Context, logger *slog.Logger) response.Middleware {
|
|||||||
args := []any{
|
args := []any{
|
||||||
"id", <-reqIDCh,
|
"id", <-reqIDCh,
|
||||||
"url", r.URL,
|
"url", r.URL,
|
||||||
|
"method", r.Method,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug("Request", args...)
|
logger.Debug("Request", args...)
|
||||||
|
|||||||
@@ -67,12 +67,12 @@ func (b bodyRes) GetStatus() (int, bool) {
|
|||||||
return b.res.GetStatus()
|
return b.res.GetStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b bodyRes) getRedirect() (code redirect.Code, to string, ok bool) {
|
func (b bodyRes) GetRedirect() (code redirect.Code, to string, ok bool) {
|
||||||
if b.res == nil {
|
if b.res == nil {
|
||||||
return 0, "", false
|
return 0, "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.res.getRedirect()
|
return b.res.GetRedirect()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b bodyRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
func (b bodyRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
||||||
|
|||||||
@@ -66,12 +66,12 @@ func (c cookieRes) GetStatus() (int, bool) {
|
|||||||
return c.res.GetStatus()
|
return c.res.GetStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c cookieRes) getRedirect() (code redirect.Code, to string, ok bool) {
|
func (c cookieRes) GetRedirect() (code redirect.Code, to string, ok bool) {
|
||||||
if c.res == nil {
|
if c.res == nil {
|
||||||
return 0, "", false
|
return 0, "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.res.getRedirect()
|
return c.res.GetRedirect()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c cookieRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
func (c cookieRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"ruben/inventory2/internal/consts"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -133,3 +134,27 @@ func GetError(err error) (e ErrorResponse, ok bool) {
|
|||||||
ok = errors.As(err, &e)
|
ok = errors.As(err, &e)
|
||||||
return e, ok
|
return e, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// error wrapping utilities
|
||||||
|
|
||||||
|
func ErrorFromConstant(err error) error {
|
||||||
|
cerr := err
|
||||||
|
for cerr != nil {
|
||||||
|
switch cerr {
|
||||||
|
case consts.ErrNotFound:
|
||||||
|
return NotFound()
|
||||||
|
case consts.ErrConflict:
|
||||||
|
return Conflict()
|
||||||
|
}
|
||||||
|
|
||||||
|
uerr, ok := cerr.(interface {
|
||||||
|
Unwrap() error
|
||||||
|
})
|
||||||
|
if !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cerr = uerr.Unwrap()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,12 +68,12 @@ func (h htmlRes) GetStatus() (int, bool) {
|
|||||||
return h.res.GetStatus()
|
return h.res.GetStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h htmlRes) getRedirect() (code redirect.Code, to string, ok bool) {
|
func (h htmlRes) GetRedirect() (code redirect.Code, to string, ok bool) {
|
||||||
if h.res == nil {
|
if h.res == nil {
|
||||||
return 0, "", false
|
return 0, "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.res.getRedirect()
|
return h.res.GetRedirect()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h htmlRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
func (h htmlRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
||||||
|
|||||||
@@ -69,12 +69,12 @@ func (j jsonRes) GetStatus() (int, bool) {
|
|||||||
return j.res.GetStatus()
|
return j.res.GetStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j jsonRes) getRedirect() (code redirect.Code, to string, ok bool) {
|
func (j jsonRes) GetRedirect() (code redirect.Code, to string, ok bool) {
|
||||||
if j.res == nil {
|
if j.res == nil {
|
||||||
return 0, "", false
|
return 0, "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
return j.res.getRedirect()
|
return j.res.GetRedirect()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j jsonRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
func (j jsonRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func (r redirectRes) GetStatus() (int, bool) {
|
|||||||
return r.res.GetStatus()
|
return r.res.GetStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r redirectRes) getRedirect() (code redirect.Code, to string, ok bool) {
|
func (r redirectRes) GetRedirect() (code redirect.Code, to string, ok bool) {
|
||||||
return r.code, r.to, true
|
return r.code, r.to, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type (
|
|||||||
|
|
||||||
GetStatus() (code int, ok bool)
|
GetStatus() (code int, ok bool)
|
||||||
getBody() (body io.ReadCloser, ok bool, err error)
|
getBody() (body io.ReadCloser, ok bool, err error)
|
||||||
getRedirect() (code redirect.Code, to string, ok bool)
|
GetRedirect() (code redirect.Code, to string, ok bool)
|
||||||
getCookies() []http.Cookie
|
getCookies() []http.Cookie
|
||||||
getContentType() (contentType string, ok bool)
|
getContentType() (contentType string, ok bool)
|
||||||
|
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ func (s statusRes) GetStatus() (int, bool) {
|
|||||||
return s.code, true
|
return s.code, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statusRes) getRedirect() (code redirect.Code, to string, ok bool) {
|
func (s statusRes) GetRedirect() (code redirect.Code, to string, ok bool) {
|
||||||
if s.res == nil {
|
if s.res == nil {
|
||||||
return 0, "", false
|
return 0, "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.res.getRedirect()
|
return s.res.GetRedirect()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s statusRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
func (s statusRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func Write(w http.ResponseWriter, r *http.Request, res Response) {
|
|||||||
hdrs.Add("Content-Type", ct)
|
hdrs.Add("Content-Type", ct)
|
||||||
}
|
}
|
||||||
|
|
||||||
if code, to, ok := res.getRedirect(); ok {
|
if code, to, ok := res.GetRedirect(); ok {
|
||||||
http.Redirect(w, r, to, code.Int())
|
http.Redirect(w, r, to, code.Int())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"ruben/inventory2/internal/consts"
|
||||||
"ruben/inventory2/internal/server/response"
|
"ruben/inventory2/internal/server/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,27 +16,642 @@ type (
|
|||||||
Mux struct {
|
Mux struct {
|
||||||
Mux *http.ServeMux
|
Mux *http.ServeMux
|
||||||
middleware []response.Middleware
|
middleware []response.Middleware
|
||||||
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewMux(ms ...response.Middleware) *Mux {
|
var (
|
||||||
|
ErrHandlerNotFound = fmt.Errorf("%w: handler not found", consts.ErrNotFound)
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMux(log *slog.Logger, ms ...response.Middleware) *Mux {
|
||||||
return &Mux{
|
return &Mux{
|
||||||
Mux: http.NewServeMux(),
|
Mux: http.NewServeMux(),
|
||||||
middleware: ms,
|
middleware: ms,
|
||||||
|
log: log,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Mux) AddMiddleware(ms ...response.Middleware) *Mux {
|
||||||
|
m.middleware = append(m.middleware, ms...)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mux) traced(method, msg string, args ...any) func(func() (finalArgs []any)) {
|
||||||
|
return logTraceAndDefer(m.log, "Mux", method, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Mux) Handle(pattern string, fn response.HandlerFunc) {
|
func (m *Mux) Handle(pattern string, fn response.HandlerFunc) {
|
||||||
for _, mw := range m.middleware {
|
defer m.traced("Handle", "", "pattern", pattern)(nil)
|
||||||
|
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) {
|
||||||
|
defer m.traced("Route", "pattern", pattern)(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
|
||||||
|
}
|
||||||
|
logTrace(m.log, "mux", "Route", "cleanPattern: "+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 *slog.Logger
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSubMux(log *slog.Logger, ms ...response.Middleware) *SubMux {
|
||||||
|
return &SubMux{
|
||||||
|
tree: newMuxTree(log.WithGroup("muxTree")),
|
||||||
|
middleware: ms,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SubMux) traced(method, msg string, args ...any) func(func() (finalArgs []any)) {
|
||||||
|
return logTraceAndDefer(m.log, "SubMux", method, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this is capturing all subroutes!
|
||||||
|
func (m *SubMux) Handle(pattern string, fn response.HandlerFunc) {
|
||||||
|
defer m.traced("Handle", "", "pattern", pattern)(nil)
|
||||||
|
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) {
|
||||||
|
//defer m.traced("Route", "", "basePathPattern", basePathPattern)(nil)
|
||||||
|
defer m.traced("Route", "", "pattern", pattern)(nil)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
logTrace(m.log, "SubMux", "Route", "Handle about to be called", "cleanPattern", 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 *slog.Logger, numPatternSegments int, sr Subrouter) response.HandlerFunc {
|
||||||
|
defer logTraceAndDefer(logger, "", "buildSubrouterHandlerFunc", "", "numPatternSegments", numPatternSegments)(nil)
|
||||||
|
return func(r *http.Request) (res response.Response, err error) {
|
||||||
|
defer logTraceAndDefer(logger, "", "buildSubrouterHandlerFunc", "", "numPatternSegments", numPatternSegments, "r.URL", r.URL)(func() []any {
|
||||||
|
return []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
|
||||||
|
|
||||||
|
logTrace(logger, "", "buildSubrouterHandlerFunc", "handler called", "subpath", r.URL.Path)
|
||||||
|
fn, params, ok := sr.Handler(r)
|
||||||
|
logTrace(logger, "", "buildSubrouterHandlerFunc", "handler returned", "subpath", r.URL.Path, "fn", fn, "params", params, "ok", 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) {
|
||||||
|
defer m.traced("Handler", "", "r.URL", r.URL)(func() []any {
|
||||||
|
return []any{
|
||||||
|
"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 *slog.Logger
|
||||||
|
|
||||||
|
branches map[string]*muxTree
|
||||||
|
wildcardKey string
|
||||||
|
wildcardBranch *muxTree
|
||||||
|
|
||||||
|
method string
|
||||||
|
handler response.HandlerFunc
|
||||||
|
subrouteHandler response.HandlerFunc
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func newMuxTree(log *slog.Logger) *muxTree {
|
||||||
|
return &muxTree{
|
||||||
|
log: log,
|
||||||
|
branches: make(map[string]*muxTree),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *muxTree) traced(method, msg string, args ...any) func(func() (finalArgs []any)) {
|
||||||
|
return logTraceAndDefer(m.log, "muxTree", method, msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
defer m.traced("set", "", "pattern", pattern)(func() []any {
|
||||||
|
return []any{
|
||||||
|
"muxTree", m,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
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) {
|
||||||
|
defer m.traced(
|
||||||
|
"setBySegments",
|
||||||
|
"",
|
||||||
|
"method", method,
|
||||||
|
"segments", segments,
|
||||||
|
"trailingSlash", trailingSlash,
|
||||||
|
"endOfURLWildcard", endOfURLWildcard,
|
||||||
|
)(func() []any {
|
||||||
|
return []any{
|
||||||
|
"muxTree", m,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
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) {
|
||||||
|
defer m.traced("get", "", "method", method, "pattern", pattern)(func() []any {
|
||||||
|
return []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) {
|
||||||
|
defer m.traced("getByHTTPMethodAndSegments", "", "method", method, "segments", segments, "trailingSlash", trailingSlash)(func() []any {
|
||||||
|
return []any{
|
||||||
|
"fn", fn,
|
||||||
|
"params", params,
|
||||||
|
"found", found,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if len(segments) == 0 {
|
||||||
|
logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "no segments", "tree", m)
|
||||||
|
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 {
|
||||||
|
logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "matching branch found", "head", head, "sm", sm)
|
||||||
|
//return sm.getByHTTPMethodAndSegments(method, tail, trailingSlash)
|
||||||
|
if fn, params, found = sm.getByHTTPMethodAndSegments(method, tail, trailingSlash); found {
|
||||||
|
return fn, params, found
|
||||||
|
}
|
||||||
|
logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "matching branch mismatched at subpath")
|
||||||
|
} else {
|
||||||
|
logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "no matching branch found")
|
||||||
|
}
|
||||||
|
if sm := m.wildcardBranch; sm != nil {
|
||||||
|
logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "wildcard branch found", "m.wildcardKey", m.wildcardKey, "sm", sm)
|
||||||
|
//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
|
||||||
|
}
|
||||||
|
logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "wildcard branch mismatched at subpath")
|
||||||
|
} else {
|
||||||
|
logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "no wildcard branch found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TODO: test ---
|
||||||
|
if m.subrouteHandler != nil && (m.method == method || m.method == "") {
|
||||||
|
logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "subroutes captured", "m", m)
|
||||||
|
return m.subrouteHandler, nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "subroutes not captured", "m", m)
|
||||||
|
// ---
|
||||||
|
|
||||||
|
return nil, map[string]string{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHTTPMethodAndPathSegmentsDroppingEndOfURLWildcard(logger *slog.Logger, pattern string) (method string, segments []string, trailingSlash, endOfURLWildcard bool) {
|
||||||
|
defer logTraceAndDefer(logger, "", "getHTTPMethodAndPathSegmentsDroppingEndOfURLWildcard", "", "pattern", pattern)(func() []any {
|
||||||
|
return []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 *slog.Logger, pattern string) (method string, segments []string, trailingSlash bool) {
|
||||||
|
defer logTraceAndDefer(logger, "", "getHTTPMethodAndPathSegments", "", "pattern", pattern)(func() []any {
|
||||||
|
return []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 *slog.Logger, pathPattern string) (segments []string, trailingSlash bool) {
|
||||||
|
defer logTraceAndDefer(logger, "", "getPathSegments", "", "pathPattern", pathPattern)(func() []any {
|
||||||
|
return []any{
|
||||||
|
"segments", segments,
|
||||||
|
"trailingSlash", trailingSlash,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
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 *slog.Logger, pathPattern string) (segments []string, trailingSlash, endOfURLWildcard bool) {
|
||||||
|
defer logTraceAndDefer(logger, "", "getPathSegmentsWithoutEndOfURLWildcard", "", "pathPattern", pathPattern)(func() []any {
|
||||||
|
return []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
|
prev := fn
|
||||||
fn = mw(func(r *http.Request) (response.Response, error) {
|
fn = mw(func(r *http.Request) (response.Response, error) {
|
||||||
return prev(r)
|
return prev(r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Mux.Handle(pattern, response.Handler(fn))
|
return fn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
// logging utilities
|
||||||
m.Mux.ServeHTTP(w, r)
|
|
||||||
|
func logTraceAndDefer(logger *slog.Logger, typeName, method, msg string, args ...any) func(func() (finalArgs []any)) {
|
||||||
|
fmsg := fmt.Sprintf("%s.%s called", typeName, method)
|
||||||
|
if msg != "" {
|
||||||
|
fmsg = fmt.Sprintf("%s: %s", fmsg, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log(nil, consts.LevelTrace, fmsg, args...)
|
||||||
|
return func(cb func() (finalArgs []any)) {
|
||||||
|
if cb != nil {
|
||||||
|
args = append(args, cb()...)
|
||||||
|
}
|
||||||
|
fmsg := fmt.Sprintf("%s.%s returned", typeName, method)
|
||||||
|
if msg != "" {
|
||||||
|
fmsg = fmt.Sprintf("%s: %s", fmsg, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log(nil, consts.LevelTrace, fmsg, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logTrace(logger *slog.Logger, typeName, method, msg string, args ...any) {
|
||||||
|
fmsg := fmt.Sprintf("%s.%s", typeName, method)
|
||||||
|
if msg != "" {
|
||||||
|
fmsg = fmt.Sprintf("%s: %s", fmsg, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log(nil, consts.LevelTrace, fmsg, args...)
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-125
@@ -3,6 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -12,11 +13,13 @@ import (
|
|||||||
|
|
||||||
"github.com/angelbeltran/templater"
|
"github.com/angelbeltran/templater"
|
||||||
|
|
||||||
"ruben/inventory2/internal/consts"
|
|
||||||
"ruben/inventory2/internal/domains/accounts"
|
"ruben/inventory2/internal/domains/accounts"
|
||||||
"ruben/inventory2/internal/domains/authentication"
|
"ruben/inventory2/internal/domains/authentication"
|
||||||
etsy_platform "ruben/inventory2/internal/domains/platforms/etsy"
|
etsy_platform "ruben/inventory2/internal/domains/platforms/etsy"
|
||||||
"ruben/inventory2/internal/domains/raw_events"
|
"ruben/inventory2/internal/domains/raw_events"
|
||||||
|
accounts_api "ruben/inventory2/internal/server/api/accounts"
|
||||||
|
auth_api "ruben/inventory2/internal/server/api/auth"
|
||||||
|
templates_api "ruben/inventory2/internal/server/api/templates"
|
||||||
"ruben/inventory2/internal/server/middleware"
|
"ruben/inventory2/internal/server/middleware"
|
||||||
"ruben/inventory2/internal/server/response"
|
"ruben/inventory2/internal/server/response"
|
||||||
"ruben/inventory2/internal/server/router"
|
"ruben/inventory2/internal/server/router"
|
||||||
@@ -26,7 +29,7 @@ import (
|
|||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
http.Handler
|
*router.Mux
|
||||||
contentDir string
|
contentDir string
|
||||||
templater *templater.Templater
|
templater *templater.Templater
|
||||||
rawEvents *raw_events.Store
|
rawEvents *raw_events.Store
|
||||||
@@ -45,97 +48,124 @@ func NewServer(
|
|||||||
etsy *etsy_platform.Platform,
|
etsy *etsy_platform.Platform,
|
||||||
auth *authentication.Authenticator,
|
auth *authentication.Authenticator,
|
||||||
) *Server {
|
) *Server {
|
||||||
mux := router.NewMux(middleware.LogRequests(ctx, logger.WithGroup("request")))
|
mux := router.NewMux(
|
||||||
|
logger,
|
||||||
|
middleware.LogRequests(ctx, logger.WithGroup("request")),
|
||||||
|
)
|
||||||
|
|
||||||
|
tmpl := templater.NewTemplater(
|
||||||
|
contentDir+"/templates",
|
||||||
|
func() template.FuncMap {
|
||||||
|
return template.FuncMap{
|
||||||
|
// params
|
||||||
|
"addPathParam": func(k string, v any, args map[string]any) (map[string]any, error) {
|
||||||
|
pathParams, ok := args["PathParams"].(map[string]string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("PathParams no set are args: %v", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
pathParams[k] = fmt.Sprint(v)
|
||||||
|
|
||||||
|
return args, nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// parsing
|
||||||
|
"parseInt": func(s string) (int, error) {
|
||||||
|
return strconv.Atoi(s)
|
||||||
|
},
|
||||||
|
"parseInt64": func(s string) (int64, error) {
|
||||||
|
return strconv.ParseInt(s, 10, 64)
|
||||||
|
},
|
||||||
|
"parsePlatform": func(s string) (accounts.Platform, error) {
|
||||||
|
return accounts.NewPlatform(s)
|
||||||
|
},
|
||||||
|
|
||||||
|
// arithmetic
|
||||||
|
"addInt": func(a, b int) int {
|
||||||
|
return a + b
|
||||||
|
},
|
||||||
|
"subInt": func(a, b int) int {
|
||||||
|
return a - b
|
||||||
|
},
|
||||||
|
"multInt": func(a, b int) int {
|
||||||
|
return a * b
|
||||||
|
},
|
||||||
|
|
||||||
|
// json
|
||||||
|
"prettyPrintJSON": func(j json.RawMessage) string {
|
||||||
|
b, err := json.MarshalIndent(j, " ", "")
|
||||||
|
if err != nil {
|
||||||
|
return string(j)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
|
// TODO: eliminate fields that aren't needed anymore
|
||||||
log: logger,
|
log: logger,
|
||||||
Handler: mux,
|
Mux: mux,
|
||||||
contentDir: contentDir,
|
contentDir: contentDir,
|
||||||
templater: templater.NewTemplater(
|
templater: tmpl,
|
||||||
contentDir+"/templates",
|
rawEvents: rawEvents,
|
||||||
func() template.FuncMap {
|
accts: accts,
|
||||||
return template.FuncMap{
|
etsy: etsy,
|
||||||
// params
|
auth: auth,
|
||||||
"addPathParam": func(k string, v any, args map[string]any) (map[string]any, error) {
|
|
||||||
pathParams, ok := args["PathParams"].(map[string]string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("PathParams no set are args: %v", args)
|
|
||||||
}
|
|
||||||
|
|
||||||
pathParams[k] = fmt.Sprint(v)
|
|
||||||
|
|
||||||
return args, nil
|
|
||||||
},
|
|
||||||
|
|
||||||
// parsing
|
|
||||||
"parseInt": func(s string) (int, error) {
|
|
||||||
return strconv.Atoi(s)
|
|
||||||
},
|
|
||||||
"parseInt64": func(s string) (int64, error) {
|
|
||||||
return strconv.ParseInt(s, 10, 64)
|
|
||||||
},
|
|
||||||
"parsePlatform": func(s string) (accounts.Platform, error) {
|
|
||||||
return accounts.NewPlatform(s)
|
|
||||||
},
|
|
||||||
|
|
||||||
// arithmetic
|
|
||||||
"addInt": func(a, b int) int {
|
|
||||||
return a + b
|
|
||||||
},
|
|
||||||
"subInt": func(a, b int) int {
|
|
||||||
return a - b
|
|
||||||
},
|
|
||||||
"multInt": func(a, b int) int {
|
|
||||||
return a * b
|
|
||||||
},
|
|
||||||
|
|
||||||
// json
|
|
||||||
"prettyPrintJSON": func(j json.RawMessage) string {
|
|
||||||
b, err := json.MarshalIndent(j, " ", "")
|
|
||||||
if err != nil {
|
|
||||||
return string(j)
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
rawEvents: rawEvents,
|
|
||||||
accts: accts,
|
|
||||||
etsy: etsy,
|
|
||||||
auth: auth,
|
|
||||||
// TODO: shouldn't this ACTUALLY be a middleware?
|
// 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
|
||||||
authMiddleware: middleware.NewAuth(
|
authMiddleware: middleware.NewAuth(
|
||||||
logger.WithGroup("auth-middleware"),
|
logger.WithGroup("auth-middleware"),
|
||||||
auth,
|
auth,
|
||||||
newLoginURL,
|
auth_api.NewLoginURL,
|
||||||
accts,
|
accts,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
withAuth := func(fn response.HandlerFunc) response.HandlerFunc {
|
// kind of a dumb way to capture routes for webpages
|
||||||
return s.authMiddleware.AuthenticateAndAddIdentity(fn)
|
wpr := templates_api.NewWebpageRouter(
|
||||||
}
|
s.log.WithGroup("templates"),
|
||||||
|
contentDir,
|
||||||
|
tmpl,
|
||||||
|
rawEvents,
|
||||||
|
accts,
|
||||||
|
etsy,
|
||||||
|
s.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
|
||||||
|
}
|
||||||
|
|
||||||
// login
|
renderWebPage, pathParams, found := wpr.Handler(r)
|
||||||
|
if !found {
|
||||||
|
return nil, response.NotFound()
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: consider a separate '/login or /auth' router
|
for k, v := range pathParams {
|
||||||
mux.Handle("GET /login", s.loginPage)
|
r.SetPathValue(k, v)
|
||||||
mux.Handle("GET /login/callback", s.loginCallback)
|
}
|
||||||
mux.Handle("GET /logout", s.logoutPage)
|
|
||||||
|
|
||||||
// TODO: consider a separate '/accounts' router
|
return renderWebPage(r)
|
||||||
// /accounts
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
mux.Handle("POST /accounts", withAuth(s.createAccount))
|
// api endpoints
|
||||||
mux.Handle("POST /accounts/{acctID}/inventory/sync-groups/draft/listings", withAuth(s.createSyncGroupListingDraft))
|
|
||||||
mux.Handle("PUT /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/shop", withAuth(s.setShopInSyncGroupListingDraft))
|
mux.Route("/auth", auth_api.NewLoginSubrouter(s.log.WithGroup("/auth"), auth))
|
||||||
mux.Handle("PUT /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/listing", withAuth(s.setListingInSyncGroupListingDraft))
|
mux.Route("/accounts", accounts_api.NewAccountSubrouter(
|
||||||
mux.Handle("DELETE /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}", withAuth(s.deleteSyncGroupListingDraft))
|
s.log.WithGroup("/accounts"),
|
||||||
mux.Handle("POST /accounts/{acctID}/inventory/sync-groups", withAuth(s.saveNewSyncGroup))
|
s.accts,
|
||||||
|
s.authMiddleware,
|
||||||
|
))
|
||||||
|
|
||||||
|
// api webhooks (TODO: make a router for these)
|
||||||
|
|
||||||
// TODO: consider a webhook separate router
|
|
||||||
// webhooks
|
|
||||||
webhookHandler := http.StripPrefix("/webhooks", webhooks.New(
|
webhookHandler := http.StripPrefix("/webhooks", webhooks.New(
|
||||||
logger.WithGroup("webhooks"),
|
logger.WithGroup("webhooks"),
|
||||||
rawEvents,
|
rawEvents,
|
||||||
@@ -150,8 +180,6 @@ func NewServer(
|
|||||||
mux.Mux.Handle("POST /webhooks/", webhookHandler)
|
mux.Mux.Handle("POST /webhooks/", webhookHandler)
|
||||||
mux.Mux.Handle("PUT /webhooks/", webhookHandler)
|
mux.Mux.Handle("PUT /webhooks/", webhookHandler)
|
||||||
|
|
||||||
// TODO: consider a separate webpage router
|
|
||||||
|
|
||||||
// webpage content
|
// webpage content
|
||||||
|
|
||||||
// non-html content: scripts, styles, images, etc
|
// non-html content: scripts, styles, images, etc
|
||||||
@@ -165,56 +193,11 @@ func NewServer(
|
|||||||
scfs.ServeHTTP(w, r)
|
scfs.ServeHTTP(w, r)
|
||||||
})))
|
})))
|
||||||
mux.Mux.Handle("GET /styles/", http.StripPrefix("/styles", http.FileServer(http.Dir(contentDir+"/styles"))))
|
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"))))
|
||||||
|
|
||||||
// html
|
// html
|
||||||
|
|
||||||
// non-authenticated
|
mux.Route("/", wpr)
|
||||||
mux.Handle("GET /{$}", s.authMiddleware.AddIdentity(s.serveTemplates))
|
|
||||||
// authenticated
|
|
||||||
mux.Handle("GET /", withAuth(s.serveTemplates))
|
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapConstantErrorsToHTTPErrors(err error) error {
|
|
||||||
cerr := err
|
|
||||||
for cerr != nil {
|
|
||||||
switch cerr {
|
|
||||||
case consts.ErrNotFound:
|
|
||||||
return response.NotFound()
|
|
||||||
case consts.ErrConflict:
|
|
||||||
return response.Conflict()
|
|
||||||
}
|
|
||||||
|
|
||||||
uerr, ok := cerr.(interface {
|
|
||||||
Unwrap() error
|
|
||||||
})
|
|
||||||
if !ok {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cerr = uerr.Unwrap()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRequestIDProvider(ctx context.Context) <-chan int {
|
|
||||||
reqIDCh := make(chan int)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer close(reqIDCh)
|
|
||||||
|
|
||||||
nextReqID := 1
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case reqIDCh <- nextReqID:
|
|
||||||
nextReqID += 1
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return reqIDCh
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ func main() {
|
|||||||
logger := slog.New(tint.NewHandler(os.Stderr, &tint.Options{
|
logger := slog.New(tint.NewHandler(os.Stderr, &tint.Options{
|
||||||
AddSource: true,
|
AddSource: true,
|
||||||
Level: slog.LevelDebug,
|
Level: slog.LevelDebug,
|
||||||
|
//Level: consts.LevelTrace,
|
||||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||||
// this can perform general key=value log cleanup
|
// this can perform general key=value log cleanup
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,11 @@
|
|||||||
<script src="/scripts/_hyperscript.min.js.gz"></script>
|
<script src="/scripts/_hyperscript.min.js.gz"></script>
|
||||||
<script src="https://unpkg.com/htmx-ext-path-params@2.0.0/path-params.js"></script>
|
<script src="https://unpkg.com/htmx-ext-path-params@2.0.0/path-params.js"></script>
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/favicon/site.webmanifest">
|
||||||
|
|
||||||
{{/* This is where the page_head template will be inserted into the page */}}
|
{{/* This is where the page_head template will be inserted into the page */}}
|
||||||
{{- block "head" . }}{{ end }}
|
{{- block "head" . }}{{ end }}
|
||||||
</head>
|
</head>
|
||||||
@@ -40,7 +45,7 @@
|
|||||||
{{- if not $userID }}
|
{{- if not $userID }}
|
||||||
|
|
||||||
<li class="pt-[1em] pb-[1em]">
|
<li class="pt-[1em] pb-[1em]">
|
||||||
<a href="/login" class="p-[1em] font-bold {{ if eq $path "/login" }}selected{{ end }}">
|
<a href="/auth/login" class="p-[1em] font-bold {{ if eq $path "/auth/login" }}selected{{ end }}">
|
||||||
Log In / Sign Up
|
Log In / Sign Up
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -66,7 +71,7 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="pt-[1em] pb-[1em]">
|
<li class="pt-[1em] pb-[1em]">
|
||||||
<a href="/logout" class="p-[1em] font-bold">
|
<a href="/auth/logout" class="p-[1em] font-bold">
|
||||||
Log Out
|
Log Out
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user