package site import ( "errors" "fmt" "net/http" "net/url" "os" "path" "path/filepath" "ruben/inventory2/internal/site/middleware" "ruben/inventory2/internal/site/response" "strconv" "strings" ) // GET / // compiles the page template or component template matching the url func (s *Server) serveTemplates(r *http.Request) (response.Response, error) { name, args := s.getTemplateNameAndArgs(r, "internal/site/templates/component_bodies") b, err := s.templater.ExecuteComponentBody(name, args...) if err == nil { return response.HTML(b), nil } if !isFileNotFoundError(err) { return s.handleTemplateError(err, args...) } name, args = s.getTemplateNameAndArgs(r, "internal/site/templates/page_bodies") 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) { 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) { 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) { if isFileNotFoundError(err) { return nil, response.NotFound(). Wrap(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" } 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 }