subrouters

This commit is contained in:
2026-01-12 04:54:53 -07:00
parent a72fd9e0e8
commit 0d6af06d65
27 changed files with 950 additions and 174 deletions
+6
View File
@@ -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

+1
View File
@@ -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"}
+7
View File
@@ -0,0 +1,7 @@
package consts
import "log/slog"
const (
LevelTrace slog.Level = slog.LevelDebug - 4
)
+1 -1
View File
@@ -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).
+1
View File
@@ -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...)
+2 -2
View File
@@ -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) {
+2 -2
View File
@@ -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) {
+25
View File
@@ -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
}
+2 -2
View File
@@ -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) {
+2 -2
View File
@@ -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) {
+1 -1
View File
@@ -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
} }
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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) {
+1 -1
View File
@@ -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
} }
+626 -5
View File
@@ -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
View File
@@ -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
}
+1
View File
@@ -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
+7 -2
View File
@@ -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>