removed intermediate /internal directory
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
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/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
|
||||
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,
|
||||
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,
|
||||
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()
|
||||
|
||||
args := []any{
|
||||
"Request",
|
||||
r,
|
||||
// add services and data here
|
||||
"RawEvents",
|
||||
s.rawEvents.WithContext(ctx),
|
||||
"URLCalc",
|
||||
newURLCalculator(r.URL),
|
||||
"Accounts",
|
||||
s.accts.WithContext(ctx),
|
||||
"Etsy",
|
||||
s.etsy.WithContext(ctx),
|
||||
|
||||
// auth tooling
|
||||
"Identity",
|
||||
auth.GetIdentity(ctx),
|
||||
"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(`
|
||||
<section class="text-center">
|
||||
<header class="m-[1em]">
|
||||
<h2>
|
||||
Access Not Granted
|
||||
</h2>
|
||||
|
||||
<a href="/ui%s" class="block m-[1em]">
|
||||
<code>
|
||||
/ui%s
|
||||
</code>
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<div class="m-[2em]">
|
||||
<p class="italic font-bold">
|
||||
Sorry, you don't have access to the given page.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="text-center">
|
||||
<a href="/">
|
||||
Return to app
|
||||
</a>
|
||||
</section>
|
||||
`, 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
|
||||
}
|
||||
Reference in New Issue
Block a user