From e59a973228275600c2cc8b836324624a305923ad Mon Sep 17 00:00:00 2001 From: spsobole Date: Sat, 29 May 2021 15:14:15 -0600 Subject: [PATCH] Small fixes --- auth/auth.go | 5 ++- context.go | 82 ++++++++++++++++++++++++++++++++++-- server.go | 117 +++++++++++++++++++++++++++++++++++++++++++-------- utility.go | 7 +++ 4 files changed, 187 insertions(+), 24 deletions(-) create mode 100644 utility.go diff --git a/auth/auth.go b/auth/auth.go index e0a5024..df69983 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -5,8 +5,9 @@ import ( ) type AuthData struct { - User string - Group string + User string + Group string + Properties map[string]string } type AuthManager interface { diff --git a/context.go b/context.go index da3d3b3..a008b12 100644 --- a/context.go +++ b/context.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "net/http" "strconv" + "strings" "git.thirdmartini.com/pub/snap/auth" ) @@ -19,9 +20,11 @@ type Context struct { } type SnapContent struct { - User string - Theme string - Content interface{} + User string + UserProperties map[string]string + Theme string + Meta map[string]string + Content interface{} } func (c *Context) GetRequest() *http.Request { @@ -43,14 +46,32 @@ func (c *Context) Error(code int, msg string) { c.srv.renderError(c.w, code, msg) } +func (c *Context) ErrorObject(code int, obj interface{}) { + msg, err := json.Marshal(obj) + if err != nil { + c.Error(http.StatusInternalServerError, "Internal Server Error") + return + } + c.Error(code, string(msg)) +} + func (c *Context) Reply(msg string) { c.srv.reply(c.w, msg) } +func (c *Context) ReplyWithHeaders(msg string, kv map[string]string) { + h := c.w.Header() + for k, v := range kv { + h.Add(k, v) + } + c.srv.reply(c.w, msg) +} + func (c *Context) ReplyObject(obj interface{}) { msg, err := json.Marshal(obj) if err != nil { - c.Error(400, "Internal Server Error") + c.Error(http.StatusInternalServerError, "Internal Server Error") + return } c.Reply(string(msg)) } @@ -72,11 +93,38 @@ func (c *Context) RenderEx(tmpl string, content interface{}) { cnt := SnapContent{ User: c.GetUser(), Theme: c.srv.theme, + Meta: c.srv.meta, Content: content, } c.srv.render(c.w, tmpl, &cnt) } +func (c *Context) RenderWithMeta(tmpl string, meta map[string]string, content interface{}) { + // Merge metas + if meta == nil { + meta = make(map[string]string) + } + + for k, v := range c.srv.meta { + if _, ok := meta[k]; !ok { + meta[k] = v + } + } + + cnt := SnapContent{ + User: c.GetUser(), + Theme: c.srv.theme, + Meta: meta, + Content: content, + } + + if c.auth != nil { + cnt.UserProperties = c.auth.Properties + } + + c.srv.render(c.w, tmpl, &cnt) +} + func (c *Context) GetVar(k string) (string, bool) { v, ok := c.vars[k] return v, ok @@ -90,6 +138,18 @@ func (c *Context) GetVarDefault(k string, def string) string { return v } +func (c *Context) GetVarAsSlice(k string, delim string, def []string) []string { + v, ok := c.vars[k] + if !ok { + return def + } + r := strings.Split(v, delim) + if len(r) == 1 && r[0] == "" { + return def + } + return r +} + func (c *Context) ParseForm() error { return c.r.ParseForm() } @@ -109,6 +169,15 @@ func (c *Context) FormValueUint64(k string) uint64 { return val } +func (c *Context) FormValueAsSlice(k string, delim string) []string { + v := c.r.FormValue(k) + r := strings.Split(v, delim) + if len(r) == 1 && r[0] == "" { + return []string{} + } + return r +} + func (c *Context) Redirect(url string) { http.Redirect(c.w, c.r, url, http.StatusSeeOther) } @@ -117,6 +186,11 @@ func (c *Context) QueryValue(key string) string { return c.r.URL.Query().Get(key) } +func (c *Context) QueryHas(key string) bool { + _, ok := c.r.URL.Query()[key] + return ok +} + func (c *Context) QueryValueWithDefault(key string, def string) string { val := c.r.URL.Query().Get(key) if val == "" { diff --git a/server.go b/server.go index 8202735..751d7ce 100644 --- a/server.go +++ b/server.go @@ -20,15 +20,17 @@ import ( ) type Server struct { - address string - theme string - debug bool - fs http.FileSystem - auth auth.Authenticator - router *mux.Router - templates string - cachedTmpl *template.Template + address string + theme string + debug bool + fs http.FileSystem + auth auth.Authenticator + router *mux.Router + templates string + cachedTmpl *template.Template + templateFuncs template.FuncMap + meta map[string]string // testModeEnabled if test mode is enabled we will not render the template but jsut return the // json object testModeEnabled bool @@ -39,6 +41,14 @@ type SnapBaseContent struct { Content interface{} } +func noescape(str string) template.HTML { + return template.HTML(str) +} + +var builtinFuncMap = template.FuncMap{ + "noescape": noescape, +} + func (s *Server) makeContext(auth *auth.AuthData, w http.ResponseWriter, r *http.Request) *Context { c := &Context{ r: r, @@ -50,9 +60,30 @@ func (s *Server) makeContext(auth *auth.AuthData, w http.ResponseWriter, r *http return c } +func (s *Server) withLoginHandler(auth auth.Authenticator, loginHandler func(c *Context) bool, handle func(c *Context)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if s.debug { + log.Debug("authenticated request with login ui handler: ", r.RequestURI) + } + rec, ok := auth.DoAuth(w, r) + if !ok { + log.Debug("authenticated request with login ui handler to login ", r.RequestURI) + c := s.makeContext(rec, w, r) + if loginHandler(c) { + handle(c) + } + } else { + c := s.makeContext(rec, w, r) + handle(c) + } + } +} + func (s *Server) authenticated(auth auth.Authenticator, redirect string, handle func(c *Context)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - log.Debug("authenticated request: ", r.RequestURI) + if s.debug { + log.Debug("authenticated request: ", r.RequestURI) + } rec, ok := auth.DoAuth(w, r) if !ok { @@ -70,7 +101,9 @@ func (s *Server) authenticated(auth auth.Authenticator, redirect string, handle func (s *Server) wrapper(handle func(c *Context)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - log.Debug("request: ", r.RequestURI) + if s.debug { + log.Debug("request: ", r.RequestURI) + } c := s.makeContext(nil, w, r) if s.auth != nil { @@ -119,7 +152,7 @@ func (s *Server) parseTemplates(t *template.Template, filenames ...string) (*tem // works. Otherwise we create a new template associated with t. var tmpl *template.Template if t == nil { - t = template.New(name) + t = template.New(name).Funcs(s.templateFuncs) } if name == t.Name() { tmpl = t @@ -165,7 +198,7 @@ func Walk(fs http.FileSystem, base string, walkFunc func(path string, info os.Fi } func (s *Server) LoadTemplatesFS(fs http.FileSystem, base string) (*template.Template, error) { - tmpl := template.New("") + tmpl := template.New("").Funcs(s.templateFuncs) err := Walk(fs, base, func(path string, info os.FileInfo, err error) error { if strings.Contains(path, ".html") { @@ -236,6 +269,13 @@ func (s *Server) HandleFuncAuthenticated(path, redirect string, f func(c *Contex return s.router.HandleFunc(path, s.authenticated(s.auth, redirect, f)) } +func (s *Server) HandleFuncAuthenticatedWithLogin(path string, loginHandler func(c *Context) bool, contentHandler func(c *Context)) *mux.Route { + if s.auth == nil { + return nil + } + return s.router.HandleFunc(path, s.withLoginHandler(s.auth, loginHandler, contentHandler)) +} + func (s *Server) HandleFuncCustomAuth(auth auth.Authenticator, path, redirect string, f func(c *Context)) *mux.Route { if auth == nil { log.Warn("Nil auth on", path) @@ -318,6 +358,45 @@ func (s *Server) WithDebug(debugURL string) *Server { return s } +func (s *Server) WithMetadata(meta map[string]string) *Server { + s.meta = meta + return s +} + +func (s *Server) WithTemplateFuncs(funcs template.FuncMap) *Server { + for k, f := range funcs { + s.templateFuncs[k] = f + } + return s +} + +func (s *Server) WithAuth(auth auth.Authenticator) *Server { + s.auth = auth + return s +} + +func (s *Server) WithHealthCheck(version, date string, status func() (bool, string)) { + s.HandleFunc("/_health", func(c *Context) { + ok, msg := status() + + hc := struct { + Version string + Date string + Status string + }{ + Version: version, + Date: date, + Status: msg, + } + + if ok { + c.ReplyObject(&hc) + return + } + c.ErrorObject(http.StatusServiceUnavailable, &hc) + }) +} + func (s *Server) Dump() { fmt.Printf(" Theme: %s\n", s.theme) fmt.Printf(" Templates: %s\n", s.templates) @@ -325,12 +404,14 @@ func (s *Server) Dump() { func New(address string, path string, auth auth.Authenticator) *Server { s := Server{ - router: mux.NewRouter(), - auth: auth, - address: address, - fs: http.FileSystem(http.Dir(path)), - templates: "/templates", - theme: "/static/css/default.css", + router: mux.NewRouter(), + auth: auth, + address: address, + fs: http.FileSystem(http.Dir(path)), + templates: "/templates", + templateFuncs: builtinFuncMap, + theme: "/static/css/default.css", + meta: make(map[string]string), } return &s } diff --git a/utility.go b/utility.go new file mode 100644 index 0000000..575f5d8 --- /dev/null +++ b/utility.go @@ -0,0 +1,7 @@ +package snap + +func Redirect(to string) func(c *Context) { + return func(c *Context) { + c.Redirect(to) + } +}