324 lines
8.1 KiB
Go
324 lines
8.1 KiB
Go
package templates
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"ruben/inventory2/internal/domains/accounts"
|
|
etsy_platform "ruben/inventory2/internal/domains/platforms/etsy"
|
|
"ruben/inventory2/internal/domains/raw_events"
|
|
"ruben/inventory2/internal/logging"
|
|
"ruben/inventory2/internal/server/middleware"
|
|
"ruben/inventory2/internal/server/response"
|
|
|
|
"github.com/angelbeltran/templater"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type (
|
|
webpageRouter struct {
|
|
log *logging.Logger
|
|
contentDir string
|
|
templater *templater.Templater
|
|
rawEvents *raw_events.Store
|
|
accts *accounts.Store
|
|
etsy *etsy_platform.Platform
|
|
authMiddleware *middleware.Auth
|
|
}
|
|
|
|
// ErrTemplateNotFound is returned if the reason the template failed to compile
|
|
// is due to the template not being found.
|
|
ErrTemplateNotFound struct {
|
|
err error
|
|
}
|
|
)
|
|
|
|
func SetupRoutes(
|
|
logger *logging.Logger,
|
|
r gin.IRouter,
|
|
contentDir string,
|
|
tmpl *templater.Templater,
|
|
rawEvents *raw_events.Store,
|
|
accts *accounts.Store,
|
|
etsy *etsy_platform.Platform,
|
|
authMiddleware *middleware.Auth,
|
|
) {
|
|
s := &webpageRouter{
|
|
log: logger,
|
|
contentDir: contentDir,
|
|
templater: tmpl,
|
|
rawEvents: rawEvents,
|
|
accts: accts,
|
|
etsy: etsy,
|
|
authMiddleware: authMiddleware,
|
|
}
|
|
|
|
fn2 := s.authMiddleware.AuthenticateAndAddIdentity(s.serveTemplates)
|
|
|
|
r.GET("/*rest", response.Handler(func(c *gin.Context) (response.Response, error) {
|
|
c.Request.URL.Path = c.Request.URL.Path[3:]
|
|
defer func() {
|
|
c.Request.URL.Path = "/ui" + c.Request.URL.Path
|
|
}()
|
|
|
|
p := c.Request.URL.Path
|
|
if p == "/" || p == "" {
|
|
c, err := s.authMiddleware.AddIdentityToRequest(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.serveTemplates(c)
|
|
}
|
|
return fn2(c)
|
|
}))
|
|
}
|
|
|
|
// GET /
|
|
// compiles the page template or component template matching the url
|
|
// func (s *Server) serveTemplates(r *http.Request) (response.Response, error) {
|
|
func (s *webpageRouter) serveTemplates(c *gin.Context) (response.Response, error) {
|
|
r := c.Request
|
|
name, args := s.getTemplateNameAndArgs(r, s.contentDir+"/templates/components")
|
|
fmt.Println("name:", name)
|
|
fmt.Println("args:", args)
|
|
|
|
// TODO: update
|
|
b, err := s.templater.ExecuteComponent(name, args...)
|
|
if err == nil {
|
|
return response.HTML(b), nil
|
|
}
|
|
|
|
if !isFileNotFoundError(err) {
|
|
return s.handleTemplateError(err, args...)
|
|
}
|
|
|
|
name, args = s.getTemplateNameAndArgs(r, s.contentDir+"/templates/pages")
|
|
fmt.Println("name (page):", name)
|
|
fmt.Println("args (page):", args)
|
|
|
|
if b, err = s.templater.ExecutePage(name, args...); err == nil {
|
|
return response.HTML(b), nil
|
|
}
|
|
|
|
return s.handleTemplateError(err, args...)
|
|
}
|
|
|
|
// 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) {
|
|
ctx := r.Context()
|
|
name, pathParams := getTemplateNameForURL(r.URL, templateDir)
|
|
|
|
return name, []any{
|
|
"Request",
|
|
r,
|
|
// add services and data here
|
|
"RawEvents",
|
|
s.rawEvents.WithContext(ctx),
|
|
"URLCalc",
|
|
newURLCalculator(r.URL),
|
|
"PathParams",
|
|
pathParams,
|
|
"Accounts",
|
|
s.accts.WithContext(ctx),
|
|
"Etsy",
|
|
s.etsy.WithContext(ctx),
|
|
|
|
// TODO: apply auth to all templates needed!
|
|
// auth tooling
|
|
/*
|
|
AccessToken string
|
|
Claims authentication.AccessTokenClaims
|
|
User accounts.OAuthUser
|
|
Account *accounts.Account
|
|
*/
|
|
"Identity",
|
|
middleware.GetIdentity(r.Context()),
|
|
"Auth",
|
|
newTemplateAuthenticator(r),
|
|
}
|
|
}
|
|
|
|
// TODO: clean this up...
|
|
// TODO: somehow tell what the path params are and pass them up.
|
|
// - then consider pushing this functionality into the template library.
|
|
//
|
|
// getComponentTemplateNameForURL eliminate any trailing .html or /, and checks for any
|
|
// file with path parameters in the name, eg '{abc}.html.tmpl', prefering exact filename matches.
|
|
func getTemplateNameForURL(u *url.URL, templateDir string) (name string, params map[string]string) {
|
|
templateDir = path.Clean(templateDir)
|
|
fp := strings.TrimPrefix(strings.TrimSuffix(strings.TrimSuffix(u.Path, ".html"), "/"), "/")
|
|
if fp == "" {
|
|
// "/" maps to "/index"
|
|
fp = "index"
|
|
}
|
|
|
|
fpParts := strings.Split(fp, "/")
|
|
res := getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(fpParts)
|
|
for _, combs := range res {
|
|
pattern := path.Join(templateDir, path.Join(combs...)) + ".html.tmpl"
|
|
|
|
matches, _ := filepath.Glob(pattern)
|
|
if len(matches) == 0 {
|
|
pattern := path.Join(templateDir, path.Join(combs...), "index") + ".html.tmpl"
|
|
matches, _ = filepath.Glob(pattern)
|
|
}
|
|
if len(matches) > 0 {
|
|
match := matches[0]
|
|
name = strings.TrimPrefix(strings.TrimSuffix(match, ".html.tmpl"), templateDir+"/")
|
|
|
|
patternParts := strings.Split(name, "/")
|
|
params = make(map[string]string)
|
|
for i, pp := range patternParts {
|
|
if strings.HasPrefix(pp, "{") && strings.HasSuffix(pp, "}") {
|
|
params[pp[1:len(pp)-1]] = fpParts[i]
|
|
}
|
|
}
|
|
|
|
return name, params
|
|
}
|
|
}
|
|
|
|
return fp, nil
|
|
}
|
|
|
|
func getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(filepathParts []string) [][]string {
|
|
switch len(filepathParts) {
|
|
case 0:
|
|
return nil
|
|
case 1:
|
|
return [][]string{
|
|
[]string{filepathParts[0]},
|
|
[]string{"{*}"},
|
|
}
|
|
default:
|
|
tailCombs := getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(filepathParts[1:])
|
|
|
|
combs := make([][]string, 2*len(tailCombs))
|
|
for i, c := range tailCombs {
|
|
combs[i*2] = append([]string{filepathParts[0]}, c...)
|
|
combs[i*2+1] = append([]string{"{*}"}, c...)
|
|
}
|
|
|
|
return combs
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
return nil, response.NotFound().
|
|
Wrap(ErrTemplateNotFound{
|
|
err: err,
|
|
}).
|
|
Msg("resource not found")
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
func isFileNotFoundError(err error) bool {
|
|
/*
|
|
var pe *os.PathError
|
|
isPathErr := errors.As(err, &pe)
|
|
|
|
return isPathErr && pe.Err != nil && pe.Err.Error() == "no such file or directory"
|
|
*/
|
|
|
|
var te *templater.ErrNotTemplateFileFound
|
|
return errors.As(err, &te)
|
|
}
|
|
|
|
func getHTTPStatusCode(err error) int {
|
|
rerr, ok := response.GetError(err)
|
|
if !ok {
|
|
return http.StatusInternalServerError
|
|
}
|
|
|
|
code, ok := rerr.GetStatus()
|
|
if !ok {
|
|
return http.StatusInternalServerError
|
|
}
|
|
|
|
return code
|
|
}
|
|
|
|
// template tooling
|
|
|
|
type URLCalculator struct {
|
|
url *url.URL
|
|
}
|
|
|
|
func newURLCalculator(u *url.URL) URLCalculator {
|
|
cpy := *u
|
|
return URLCalculator{
|
|
url: &cpy,
|
|
}
|
|
}
|
|
|
|
func (c URLCalculator) SetQueryParam(k string, v any) string {
|
|
u := *c.url
|
|
q := u.Query()
|
|
q.Set(k, fmt.Sprint(v))
|
|
u.RawQuery = q.Encode()
|
|
|
|
return u.String()
|
|
}
|
|
|
|
// template authenticator
|
|
|
|
type templateAuthenticator struct {
|
|
req *http.Request
|
|
}
|
|
|
|
func newTemplateAuthenticator(req *http.Request) *templateAuthenticator {
|
|
return &templateAuthenticator{
|
|
req: req,
|
|
}
|
|
}
|
|
|
|
// templateAuthorizationFunc these shouild always return an empty string
|
|
type templateAuthorizationFunc = func() (string, error)
|
|
|
|
func (a *templateAuthenticator) ByMatchingAccountID(acctIDPathPosition int) (string, error) {
|
|
return "", authorizeByMatchingAccountID(a.req, acctIDPathPosition)
|
|
}
|
|
|
|
func authorizeByMatchingAccountID(r *http.Request, acctIDPathPosition int) error {
|
|
pathParts := strings.Split(strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/"), "/"), "/")
|
|
if len(pathParts) < acctIDPathPosition {
|
|
return fmt.Errorf("authorization failed due to unexpected path: %s", r.URL.Path)
|
|
}
|
|
|
|
part := pathParts[acctIDPathPosition-1]
|
|
acctID, err := strconv.ParseInt(part, 10, 64)
|
|
if err != nil {
|
|
return response.NotFound().
|
|
Msgf("account does not exist: %s", part)
|
|
}
|
|
|
|
id := middleware.GetIdentity(r.Context())
|
|
if id.Account == nil || id.Account.AccountID != acctID {
|
|
return response.Unauthorized().
|
|
Msgf("user does not have access to account %d", acctID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e ErrTemplateNotFound) Error() string {
|
|
if e.err != nil {
|
|
return fmt.Sprintf("template not found: %v", e.err)
|
|
}
|
|
return fmt.Sprintf("template not found")
|
|
}
|
|
|
|
func (e ErrTemplateNotFound) Unwrap() error {
|
|
return e.err
|
|
}
|