package ui import ( "encoding/json" "errors" "fmt" "html/template" "net/http" "net/url" "strconv" "strings" "github.com/angelbeltran/templater" "github.com/gin-gonic/gin" "ruben/inventory2/domains/accounts" etsy_platform "ruben/inventory2/domains/platforms/etsy" "ruben/inventory2/domains/raw_events" "ruben/inventory2/domains/reports" "ruben/inventory2/logging" "ruben/inventory2/server/auth" "ruben/inventory2/server/response" ) type ( webpageRouter struct { log *logging.Logger uiPath string templater *templater.Templater rawEvents *raw_events.Store accts *accounts.Store reports *reports.Store etsy *etsy_platform.Platform } // ErrTemplateNotFound is returned if the reason the template failed to compile // is due to the template not being found. ErrTemplateNotFound struct { err error } ) func Routes( logger *logging.Logger, r gin.IRoutes, uiPath string, rawEvents *raw_events.Store, accts *accounts.Store, reps *reports.Store, etsy *etsy_platform.Platform, authenticate gin.HandlerFunc, ) { s := &webpageRouter{ log: logger, uiPath: uiPath, templater: new(templater.Templater).With(templater.Config{ Funcs: func(name string, props map[string]any) template.FuncMap { return template.FuncMap{ // 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) }, // strings "lowerSnakeCase": func(s string) string { return strings.ToLower(strings.Join(strings.Split(s, " "), "_")) }, "splitSnakeCase": func(s string) string { return strings.Join(strings.Split(s, "_"), " ") }, "capitalize": func(s string) string { ss := strings.Split(s, " ") us := make([]string, len(ss)) for i, v := range ss { if len(v) == 0 { us[i] = v } else { us[i] = strings.ToUpper(v[:1]) + v[1:] } } return strings.Join(us, " ") }, "hasPrefix": func(s, prefix string) bool { return strings.HasPrefix(s, prefix) }, // 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 }, // html "rawHTML": func(s string) template.HTML { return template.HTML(s) }, "rawHTMLAttr": func(s string) template.HTMLAttr { return template.HTMLAttr(s) }, "style": func(kvs ...string) (template.HTMLAttr, error) { if len(kvs)%2 != 0 { return "", fmt.Errorf("expected an even number of keys: %d", len(kvs)) } parts := make([]string, len(kvs)/2) for i := range parts { parts[i] = fmt.Sprintf("%s: %s;", kvs[2*i], kvs[2*i+1]) } return template.HTMLAttr(strings.Join(parts, " ")), nil }, // json "prettyPrintJSON": func(j json.RawMessage) string { b, err := json.MarshalIndent(j, " ", "") if err != nil { return string(j) } return string(b) }, "marshalJSON": json.Marshal, // slices "newSlice": func(args ...any) []any { return args }, "newHTMLSlice": func(args ...string) []template.HTML { ss := make([]template.HTML, len(args)) for i, s := range args { ss[i] = template.HTML(s) } return ss }, } }, }), rawEvents: rawEvents, accts: accts, reports: reps, etsy: etsy, } r.GET("", response.Handler(s.redirectToAccountsIfLoggedInWithAnAccount), response.Handler(s.serveTemplate)) r.GET("/*rest", authenticate, response.Handler(s.serveTemplate)) } func (s *webpageRouter) redirectToAccountsIfLoggedInWithAnAccount(c *gin.Context) (response.Response, error) { id := auth.GetIdentity(c) if id.Account == nil || id.Account.AccountID == 0 { return nil, nil } return response.TemporaryRedirect(fmt.Sprintf("%s/accounts/%d", s.uiPath, id.Account.AccountID)), nil } // GET / // compiles the page template or component template matching the url func (s *webpageRouter) serveTemplate(c *gin.Context) (response.Response, error) { // trim the url path prefix before loading the templates c.Request.URL.Path = c.Request.URL.Path[len(s.uiPath):] defer func() { c.Request.URL.Path = s.uiPath + c.Request.URL.Path }() r := c.Request ctx := r.Context() id := auth.GetIdentity(ctx) var mockMode bool if id.Account != nil { var err error if mockMode, err = s.accts.GetMockMode(ctx, id.Account.AccountID); err != nil { return nil, err } } args := []any{ "Request", r, // add services and data here "RawEvents", s.rawEvents.WithContext(ctx), "URLCalc", newURLCalculator(r.URL), "Accounts", s.accts.WithContext(ctx), "Reports", s.reports.WithContext(ctx), "Etsy", s.etsy.WithContext(ctx), "MockMode", mockMode, // auth tooling "Identity", id, "Auth", newTemplateAuthenticator(r), } b, err := s.templater.Execute(strings.Trim(r.URL.Path, "/"), args...) if err != nil { if isFileNotFoundError(err) || isInvalidWildcardValue(err) { werr := response.NotFound(). Wrap(ErrTemplateNotFound{ err: err, }). Msg("resource not found") nfb, nferr := s.templater.Execute("not-found", args...) if nferr != nil { s.log.Errorf("failed to render not-found page: %v", nferr) return nil, werr } return nil, werr. HTML(nfb) } return nil, err } return response.HTML(b), nil } func isInvalidWildcardValue(err error) bool { var te *templater.ErrInvalidWildcardValue return errors.As(err, &te) } func isFileNotFoundError(err error) bool { var te *templater.ErrNotTemplateFileFound return errors.As(err, &te) } // 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 := auth.GetIdentity(r.Context()) if id.Account == nil || id.Account.AccountID != acctID { return response.Unauthorized(). HTML([]byte(fmt.Sprintf(`

Access Not Granted

/ui%s

Sorry, you don't have access to the given page.

Return to app
`, r.URL, r.URL))) } 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 }