Conditional rendering of HTML in Golang layout.tpl by session variable - html

I use Gorilla sessions (via negroni-sessions) to store my user sessions in cookies. I also use github.com/unrolled/render for my HTML template rendering:
main.go:
package main
import (
...
"github.com/codegangsta/negroni"
"github.com/goincremental/negroni-sessions"
"github.com/goincremental/negroni-sessions/cookiestore"
"github.com/julienschmidt/httprouter"
"github.com/unrolled/render"
...
)
func init() {
...
ren = render.New(render.Options{
Directory: "templates",
Layout: "layout",
Extensions: []string{".html"},
Funcs: []template.FuncMap{TemplateHelpers},
IsDevelopment: false,
})
...
}
func main() {
...
router := httprouter.New()
router.GET("/", HomeHandler)
// Add session store
store := cookiestore.New([]byte("my password"))
store.Options(sessions.Options{
//MaxAge: 1200,
Domain: "",
Path: "/",
})
n := negroni.New(
negroni.NewRecovery(),
sessions.Sessions("cssession", store),
negroni.NewStatic(http.Dir("../static")),
)
n.UseHandler(router)
n.Run(":9000")
}
As you can see above, I use a layout.html master HTML template which is included when any page renders, like my home page:
package main
import (
"html/template"
"github.com/julienschmidt/httprouter"
)
func HomeHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var model = struct {
CatalogPicks []PromotionalModelList
ClearanceItems []Model
}{
CatalogPicks: GetCatalogPicks(),
ClearanceItems: GetClearanceItems(),
}
ren.HTML(w, http.StatusOK, "home", model)
}
In my layout.html master HTML template, I want to render an admin menu but only if the current user is an admin:
layout.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{{ template "title" . }}</title>
...
</head>
<body>
...
<!--Main Menu-->
<nav class="menu">
<ul class="catalog">
<li class="has-submenu">
{{ RenderMenuCategories }}
</li>
<li>Blog</li>
<li>Company</li>
{{ RenderAdminMenu }}
</ul>
</nav>
...
My issue is that the above template helper function RenderAdminMenu() doesn't have access to the HTTP Request object and therefore cannot access the User session object to determine if the user is admin.
I can pass the User object into the template context via the Home page handler, and use an if statement RenderAdminMenu() function, like this
{{ if .User.IsAdmin }}
{{ RenderAdminMenu }}
{{ end }}
...but since I am using a master template, I would have to do that from every web page on the site. Is there a more efficient way?
I was thinking perhaps there might be a way to access some kind of global Context object from within RenderAdminMenu() (or layout.html) which contains the Request details (like you can in ASP.NET)

There's a few things you need to do to tie this together. I'm not going to show a complete example as it would be both fairly lengthy and may not match your code (which you haven't posted). It will contain the basic building blocks though: if you get stuck, come back with a direct question and a code snippet and you'll get a more direct answer :)
Write some middleware or logic in a [login] handler that saves the user data in the session when a user logs in. A userID, email and admin boolean value would be sufficient. e.g.
// In your login handler, once you've retrieved the user &
// matched their password hash (scrypt, of course!) from the DB.
session.Values["user"] = &youruserobject
err := session.Save(r, w)
if err != nil {
// Throw a HTTP 500
}
Note: remember that you need to gob.Register(&youruserobject{}) as per the gorilla/sessions docs if you want to store your own types.
Write a helper to type-assert your type when you pull it out of the session, e.g.
var ErrInvalidUser= errors.New("invalid user stored in session")
func GetUser(session *sessions.Session) (*User, error) {
// You can make the map key a constant to avoid typos/errors
user, ok := session.Values["user"].(*User)
if !ok || user == nil {
return nil, ErrInvalidUser
}
return user, nil
}
// Use it like this in a handler that serves user content
session, err := store.Get("yoursessionname", r)
if err != nil {
// Throw a HTTP 500
}
user, err := GetUser(session)
if err != nil {
// Re-direct back to the login page or
// show a HTTP 403 Forbidden, etc.
}
Write something to check if the returned user is an admin:
func IsAdmin(user *User) bool {
if user.Admin == true && user.ID != "" && user.Email != "" {
return true
}
return false
}
Pass that to the template:
err := template.Execute(w, "sometemplate.html", map[string]interface{}{
"admin": IsAdmin(user),
"someotherdata": someStructWithData,
}
// In your template...
{{ if .admin }}{{ template "admin_menu" }}{{ end }}
Also make sure you're setting an authentication key for your session cookies (read the gorilla docs), preferably an encryption key, and that you're serving your site over HTTPS with the Secure: true flag set as well.
Keep in mind that the above method is also simplified: if you de-flag a user as admin in your DB, the application will continue to detect them as an administrator for as long as their session lasts. By default this can be 7 days, so if you're in a risky environment where admin churn is a real problem, it may pay to have really short sessions OR hit the DB inside the IsAdmin function just to be safe. If it's a personal blog and it's just you, not so much.
Added: If you want to pass the User object directly to the template, you can do that too. Note that it's more performant to do it in your handler/middleware than it is in the template logic. You also get the flexibility of more error handling, and the option of "bailing out" earlier - i.e. if the session contains nothing, you can fire up a HTTP 500 error rather than rendering half a template or having to put lots of logic in your template to handle nil data.
You still need to store your User object (or equivalent) in the session, and retrieve it from session.Values before you can pass it to the template.
func GetUser(r *http.Request) *User {
session, err := store.Get("yoursessionname", r)
if err != nil {
// Throw a HTTP 500
}
if user, ok := session.Values["user"].(*User); ok {
return user
}
return nil
}
// In the handler itself
err := template.Execute(w, "sometemplate.html", map[string]interface{}{
"user": GetUser(r),
"someotherdata": someStructWithData,
}
// In your template...
{{ if .User.admin }}{{ template "admin_menu" }}{{ end }}

It seems you cannot access the Request context from a template or a template helper function afterall (so I accepted the answer above). My solution was to create a Page struct that I pass as the context for every template. It contains the Content as a generic interface, the User object, as well as other useful parameters:
//Page holds the model to be rendered for every HTTP handler.
type Page struct {
MetaTitle string
User User
HeaderStyles string
HeaderScripts string
FooterScripts string
Content interface{}
}
func (pg *Page) Init(r *http.Request) {
if pg.MetaTitle == "" {
pg.MetaTitle = "This is the default <title> content for the page!"
}
user, _ := GetUserFromSession(r)
pg.User = *user
if user.IsAdmin() {
pg.HeaderStyles += `<link href="/css/libs/summernote/summernote.css" rel="stylesheet">`
pg.FooterScripts += `<script src="/js/libs/summernote/summernote.min.js"></script>`
}
}
The Init method allows me to set defaults and use the Page struct more easily, by specifying only the Content for the page, if that's all I need:
package main
import (
"html/template"
"github.com/julienschmidt/httprouter"
)
func HomeHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var model = struct {
CatalogPicks []PromotionalModelList
ClearanceItems []Model
}{
CatalogPicks: GetCatalogPicks(),
ClearanceItems: GetClearanceItems(),
}
pg := &Page{Content: model}
pg.Init(r)
ren.HTML(w, http.StatusOK, "home", pg)
}

Related

How to organise Function Maps for specific endpoints in Gin (GoLang)?

TL;DR
I'm wondering how to organise function maps for go templates that are dependant on the data being obtained in the specific endpoint being called.
Issue
I have a main frontend endpoint (for a CMS) which will grab the slug of the page, and go and obtain the post data it from the model. I also have a templates package that takes in models, the post data and gin context.
GoView is currently being used. But I am setting HTMLRender to equal a new ginview evertime the page is hit presumably this isn't great for performance and I'm wondering if there is a neater way of going about it.
// Serve the front end website
func (c *FrontendController) Serve(g *gin.Context) {
path := g.Request.URL.Path
post, err := c.models.Posts.GetBySlug(path)
if err != nil {
NoPageFound(g)
return
}
tf := templates.NewFunctions(g, c.models, &post)
c.server.HTMLRender = ginview.New(goview.Config{
Root: paths.Theme(),
Extension: config.Template["file_extension"],
Master: "/layouts/main",
Partials: []string{},
DisableCache: true,
Funcs: tf.GetFunctions(),
})
r := ResourceData{
Post: post,
}
g.HTML(200, config.Template["template_dir"] + "/" + post.PageTemplate, r)
}
Templates
Some template functions include getting the field & author of the post, and seeing if the user is logged in, for example.
func (t *TemplateFunctions) isAuth() bool {
...
}
There are also template functions that aren't tied to gin.context such as getting the assets path.
func (t *TemplateFunctions) assetsPath() string {
...
}
Questions
Is there a way to merge the data dependant functions with global ones?
Is there a way to set the template functions without redeclaring the HTMLRender every time a page is hit? Or is it necessary?

Share current html response(as is) with user input intact

I am trying to achieve something similar to the Go Playground's share button.
When a button called share is clicked, the current HTML response is saved into a file. That also includes everything a user can see.
What I can do so far
- I can successfully save an HTML page into a file.
- I can get and save a whole page from a URL using HTTP Get.
Caveats
- I can't save a current webpage I am working on.
This is my code so far for getting remote URLs:
func HTTPGet(url string, timeout time.Duration) (content []byte, err error) {
request, err := http.NewRequest("POST", url, nil)
if err != nil {
return
}
ctx, cancel_func := context.WithTimeout(context.Background(), timeout)
request = request.WithContext(ctx)
response, err := http.DefaultClient.Do(request)
if err != nil {
return
}
defer response.Body.Close()
if response.StatusCode != 200 {
cancel_func()
return nil, fmt.Errorf("INVALID RESPONSE; status: %s", response.Status)
}
return ioutil.ReadAll(response.Body)
}
Hopefully this will not involve messing with the DOM manually.
There is a few ways this can be done, with some cooperation from the client.
If you want to save the current web page as it is displayed to the client (with the current form values, etc.), you have to do that with Javascript on the client side. You can get the document, serialize it in some form and send it to the server to save it.
Alternative is to generate the HTML page on the server side again, but that will not contain data user entered, or any dynamically generated HTML on the client side.
Another way is to use Javascript on the client side to collect page state including form values and data representing any dynamically generated content, send it to the server to save, and reconstruct the page as needed.

Serving static html using Gorilla mux

I'm using gorilla serve mux to serve static html files.
r := mux.NewRouter()
r.PathPrefix("/").Handler(http.FileServer(http.Dir("./public"))).Methods("GET")
I do have a Index.html file inside the public folder as well as other html files.
When browsing the site I get all the content of the folder instead of the default Index.html.
I came from C# and I know that IIS takes Index.html as default but it is possible to select any page as a default.
I wanted to know if there's a proper way to select a default page to serve in Gorilla mux without creating a custom handler/wrapper.
Maybe using a custom http.HandlerFunc would be easier:
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Here you can check if path is empty, you can render index.html
http.ServeFile(w, r, r.URL.Path)
})
You do have to make a custom handler because you want a custom behavior. Here, I just wrapped the http.FileServer handler.
Try this one:
package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
handler := mux.NewRouter()
fs := http.FileServer(http.Dir("./public"))
handler.PathPrefix("/").Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
//your default page
r.URL.Path = "/my_default_page.html"
}
fs.ServeHTTP(w, r)
})).Methods("GET")
log.Fatal(http.ListenAndServe(":8080", handler))
}
So, from the code, if the visited path is the root (/) then you rewrite the r.URL.Path to your default page, in this case my_default_page.html.
After grabthefish mentioned it i decided to check the actual code of gorilla serve mux.
This code is taken from net/http package which Gorilla mux is based on.
func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string,
redirect bool) {
const indexPage = "/index.html"
// redirect .../index.html to .../
// can't use Redirect() because that would make the path absolute,
// which would be a problem running under StripPrefix
if strings.HasSuffix(r.URL.Path, indexPage) {
localRedirect(w, r, "./")
return
}
the code request the index file to be index.html in lower case so renaming my index file solved it.
thank you grabthefish!

Can't access data from html/template Golang

I have three concatenated templates. base.html, menu.html, users.html. But when I execute these templates I can access data of context only from base.html.
Here is my Handler:
func HandlerUser(res http.ResponseWriter, req *http.Request){
if req.Method == "GET" {
context := Context{Title: "Users"}
users,err:= GetUser(0)
context.Data=map[string]interface{}{
"users": users,
}
fmt.Println(context.Data)
t,err := template.ParseFiles("public/base.html")
t,err = t.ParseFiles("public/menu.html")
t,err = t.ParseFiles("public/users.html")
err = t.Execute(res,context)
fmt.Println(err)
}
}
This is what I want to show in users template
{{ range $user := index .Data "users" }}
<tr id="user-{{ .Id }}">
<td id="cellId" >{{ $user.Id }}</td>
<td id="cellUserName" >{{ $user.UserName }}</td>
</tr>
{{ end }}
Note: I can access "{{.Title}}" that is used in base.html template.
First, you should check errors returned by the Template.ParseFiles() method. You store the returned error, but you only check it at the end (and by then it is overwritten like 3 times).
Next, never parse templates in the request handler, it's too time consuming and resource wasting. Do it once at startup (or on first demand). For details see It takes too much time when using "template" package to generate a dynamic web page to client in golang.
Next, you can parse multiple files at once, just enumerate all when passing to the Template.ParseFiles() function (there is a method and a function).
Know that Template.Execute() only executes a single (named) template. You have 3 associated templates, but only the "base.html" template is executed by your code. To execute a specific, named template, use Template.ExecuteTemplate(). For details, see Telling Golang which template to execute first.
First you should define a structure of your templates, decide which templates include others, and execute the "wrapper" template (using Template.ExecuteTemplate()). When you execute a template that invokes / includes another template, you have the possibility to tell what value (data) you what to pass to its execution. When you write {{template "something" .}}, that means you want to pass the value currently pointed by dot to the execution of the template named "something". Read more about this: golang template engine pipelines.
To learn more about template association and internals, read this answer: Go template name.
So in your case I would imagine that "base.html" is the wrapper, outer template, which includes "menu.html" and "users.html". So "base.html" should contain lines similar to this:
{{template "menu.html" .}}
{{template "users.html" .}}
The above lines will invoke and include the results of the mentioned templates, passing the data to their execution that was passed to "base.html" (if dot was not changed).
Parse the files using the template.ParseFiles() function (not method) like this:
var t *template.Template
func init() {
var err error
t, err = template.ParseFiles(
"public/base.html", "public/menu.html", "public/users.html")
if err != nil {
panic(err) // handle error
}
}
And execute it like this in your handler:
err := t.ExecuteTemplate(w, "base.html", context)

Hide HTML content if a user is logged in

I'm writing a web server in Go and was asking myself, what the conventional way of conditionally hiding a part of an HTML page is.
If I wanted a "sign in" button only to show up, when the user is NOT logged in, how would I achieve something like this?
Is it achieved with template engines or something else?
Thank you for taking the time to read and answer this :)
you just have to give a struct to your template and manage the rendering inside it.
Here is a working exemple to test:
package main
import (
"html/template"
"net/http"
)
func main() {
http.HandleFunc("/", helloHandler)
http.ListenAndServe(":8000", nil)
}
type User struct {
Name string
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
t := template.New("logged exemple")
t, _ = t.Parse(`
<html>
<head>
<title>Login test</title>
</head>
<body>
{{if .Logged}}
It's me {{ .User.Name }}
{{else}}
-- menu --
{{end}}
</body>
</html>
`)
// Put the login logic in a middleware
p := struct {
Logged bool
User *User
}{
Logged: true,
User: &User{Name: "Mario"},
}
t.Execute(w, p)
}
To manage the connexion you can use http://www.gorillatoolkit.org/pkg/sessions with https://github.com/codegangsta/negroni and create the connection logic inside a middleware.
Simply start session on user login .Set the necessary values in session and then hide the html u want with if tag in javascript using session. Its very simple and best way to solve this type of problems.
You might try something like this
<?php
if($_SESSION['user']){}else{
echo '<button onclick="yourfunction();">sign in</button>';
}
?>
Thus, if they aren't logged in, this'll show up.