From c184f999de6fbf40ff3cf6257a31fa50603c3f5a Mon Sep 17 00:00:00 2001 From: Nicolas Duhamel Date: Sat, 27 Feb 2016 21:42:43 +0100 Subject: [PATCH] Add first users handlers --- auth/middleware.go | 1 + templates/layout.tmpl | 18 +++++ templates/navbar.tmpl | 161 +++++++++++++++++++++++++++++++++++++ templates/users/login.tmpl | 1 + users/handlers.go | 100 +++++++++++++++++++++++ users/handles_test.go | 59 ++++++++++++++ web/data.go | 36 +++++++++ web/env.go | 92 +++++++++++++++++++++ web/templates.go | 51 ++++++++++++ 9 files changed, 519 insertions(+) create mode 100644 auth/middleware.go create mode 100644 templates/layout.tmpl create mode 100644 templates/navbar.tmpl create mode 100644 templates/users/login.tmpl create mode 100644 users/handlers.go create mode 100644 users/handles_test.go create mode 100644 web/data.go create mode 100644 web/env.go create mode 100644 web/templates.go diff --git a/auth/middleware.go b/auth/middleware.go new file mode 100644 index 0000000..8832b06 --- /dev/null +++ b/auth/middleware.go @@ -0,0 +1 @@ +package auth diff --git a/templates/layout.tmpl b/templates/layout.tmpl new file mode 100644 index 0000000..1f5bdfe --- /dev/null +++ b/templates/layout.tmpl @@ -0,0 +1,18 @@ + + + My Layout + + + + + + + + {{ template "navbar" $ }} + {{ yield }} + + + + + + diff --git a/templates/navbar.tmpl b/templates/navbar.tmpl new file mode 100644 index 0000000..a63a60f --- /dev/null +++ b/templates/navbar.tmpl @@ -0,0 +1,161 @@ + diff --git a/templates/users/login.tmpl b/templates/users/login.tmpl new file mode 100644 index 0000000..e00f89a --- /dev/null +++ b/templates/users/login.tmpl @@ -0,0 +1 @@ +hello wolrd diff --git a/users/handlers.go b/users/handlers.go new file mode 100644 index 0000000..5037202 --- /dev/null +++ b/users/handlers.go @@ -0,0 +1,100 @@ +package users + +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" + "github.com/mholt/binding" + "gitlab.quimbo.fr/odwrtw/canape-sql/auth" + "gitlab.quimbo.fr/odwrtw/canape-sql/web" +) + +// FormErrors map[field name]message +type FormErrors map[string]string + +// HandleFormErrors translate binding.Erros to FormErrors +func HandleFormErrors(errs binding.Errors) FormErrors { + ferrs := FormErrors{} + for _, err := range errs { + for _, field := range err.FieldNames { + if ferrs[field] == "" { + ferrs[field] = err.Message + } + } + } + return ferrs +} + +type loginForm struct { + Username string + Password string +} + +func (f *loginForm) FieldMap(r *http.Request) binding.FieldMap { + return binding.FieldMap{ + &f.Username: "username", + &f.Password: "password", + } +} + +func (f *loginForm) Validate(r *http.Request, errs binding.Errors) binding.Errors { + if f.Username == "" { + errs = append(errs, binding.Error{ + FieldNames: []string{"username"}, + Classification: "InvalidValues", + Message: "Specify a username", + }) + } + if f.Password == "" { + errs = append(errs, binding.Error{ + FieldNames: []string{"password"}, + Classification: "InvalidValues", + Message: "Specify a password", + }) + } + return errs +} + +func loginSubmitHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { + form := new(loginForm) + + errs := binding.Bind(r, form) + if errs != nil { + formErrs := HandleFormErrors(errs) + web.SetData(r, "FormErrors", formErrs) + return e.Rends(w, r, "users/login") + } + + err := e.Auth.Login(w, r, form.Username, form.Password) + if err != nil { + if err == auth.ErrInvalidPassword || err == ErrUnknownUser { + web.SetData(r, "FormErrors", FormErrors{"password": "Invalid username or password"}) + return e.Rends(w, r, "users/login") + } + return web.InternalError(err) + } + + return nil +} + +func userDetailsHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { + u, err := e.Auth.CurrentUser(w, r) + if err != nil { + return err + } + if u == nil { + return nil + } + _, ok := u.(*User) + if !ok { + return fmt.Errorf("Invalid user type") + } + return nil +} + +// SetRoutes adds users routes to the router +func SetRoutes(r *mux.Router, e *web.Env) { + r.Handle("/login", e.Handler(loginSubmitHandler)).Methods("POST").Name("loginPost") + r.Handle("/", e.Handler(userDetailsHandler)).Methods("GET").Name("UserDetails") +} diff --git a/users/handles_test.go b/users/handles_test.go new file mode 100644 index 0000000..67cabf3 --- /dev/null +++ b/users/handles_test.go @@ -0,0 +1,59 @@ +package users + +import ( + "io/ioutil" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" + "testing" + + "github.com/Sirupsen/logrus" + "github.com/jmoiron/sqlx" + + "gitlab.quimbo.fr/odwrtw/canape-sql/auth" + "gitlab.quimbo.fr/odwrtw/canape-sql/sqly" + "gitlab.quimbo.fr/odwrtw/canape-sql/web" +) + +type UserBackend struct { + db *sqlx.DB +} + +func (b *UserBackend) Get(username string) (auth.User, error) { + return Get(db, username) +} + +func createUserBackend(db *sqlx.DB) *UserBackend { + return &UserBackend{db: db} +} + +func TestLoginSubmit(t *testing.T) { + sqly.RunWithLastestMigration(db, pgdsn, t, func(db *sqlx.DB, t *testing.T) { + backend := createUserBackend(db) + authorizer := auth.New(backend, "peeper", "cookieName", "cookieKey", 10) + e := web.NewEnv(db, authorizer, logrus.NewEntry(logrus.New()), "../templates") + e.Mode = web.TestMode + e.Router.Handle("/login", e.Handler(loginSubmitHandler)).Methods("POST").Name("loginPost") + + ts := httptest.NewServer(e.Router) + defer ts.Close() + + cookieJar, _ := cookiejar.New(nil) + client := &http.Client{ + Jar: cookieJar, + } + + v := url.Values{} + v.Add("username", "plop") + v.Add("password", "ploppwd") + resp, err := client.PostForm(ts.URL+"/login", v) + if err != nil { + t.Fatal(err) + } + str, _ := ioutil.ReadAll(resp.Body) + if string(str) != `{"RouteName":"loginPost","Data":{"FormErrors":{"password":"Invalid username or password"}}}` { + t.Fatalf("Unpected content: %s", string(str)) + } + }) +} diff --git a/web/data.go b/web/data.go new file mode 100644 index 0000000..1f53915 --- /dev/null +++ b/web/data.go @@ -0,0 +1,36 @@ +package web + +import ( + "net/http" + + "github.com/gorilla/context" +) + +type contextKey int + +// RespData is key for access to response data in context +const respData contextKey = 0 + +// GetAllData return response's data +func GetAllData(r *http.Request) map[string]interface{} { + data, ok := context.GetOk(r, respData) + if !ok { + return nil + } + d, ok := data.(map[string]interface{}) + if !ok { + return nil + } + return d +} + +// SetData sets some response's data for access in template +func SetData(r *http.Request, key string, val interface{}) { + data, ok := context.GetOk(r, respData) + if !ok { + context.Set(r, respData, make(map[string]interface{})) + data = make(map[string]interface{}) + } + data.(map[string]interface{})[key] = val + context.Set(r, respData, data) +} diff --git a/web/env.go b/web/env.go new file mode 100644 index 0000000..41f8576 --- /dev/null +++ b/web/env.go @@ -0,0 +1,92 @@ +package web + +import ( + "fmt" + "net/http" + + "gitlab.quimbo.fr/odwrtw/canape-sql/auth" + + "github.com/Sirupsen/logrus" + "github.com/gorilla/mux" + "github.com/jmoiron/sqlx" + "github.com/unrolled/render" +) + +type Mode string + +const ( + ProductionMode Mode = "production" + DeveloppementMode = "developpement" + TestMode = "test" +) + +// Env describes an environement object passed to all handlers +type Env struct { + Database *sqlx.DB + Log *logrus.Entry + Router *mux.Router + Render *render.Render + Auth *auth.Authorizer + Mode Mode +} + +// NewEnv returns a new *Env +func NewEnv(db *sqlx.DB, auth *auth.Authorizer, log *logrus.Entry, templatesDir string) *Env { + return &Env{ + Database: db, + Log: log, + Router: mux.NewRouter(), + Render: render.New(render.Options{ + Directory: templatesDir, + Layout: "layout", + Funcs: tmplFuncs, + DisableHTTPErrorRendering: true, + RequirePartials: true, + }), + Auth: auth, + Mode: ProductionMode, + } +} + +// GetURL returns URL string from URL name and params +// Usefull for redirection and templates +func (e *Env) GetURL(name string, pairs ...string) (string, error) { + route := e.Router.Get(name) + if route == nil { + return "", fmt.Errorf("No route find for the given name: %s", name) + } + URL, err := route.URL(pairs...) + if err != nil { + return "", err + } + return URL.String(), nil +} + +// Handler create a new handler +func (e *Env) Handler(H func(e *Env, w http.ResponseWriter, r *http.Request) error) Handler { + return Handler{e, H} +} + +// The Handler struct that takes a configured Env and a function matching our +// handler signature +type Handler struct { + *Env + H func(e *Env, w http.ResponseWriter, r *http.Request) error +} + +// ServeHTTP allows our Handler type to satisfy http.Handler. +func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + err := h.H(h.Env, w, r) + if err != nil { + // Any error types we don't specifically look out for default + // to serving a HTTP 500 + h.Env.Log.Errorf(err.Error()) + http.Error(w, http.StatusText(http.StatusInternalServerError), + http.StatusInternalServerError) + } +} + +// InternalError create a internal error +func InternalError(err error) error { + return nil +} diff --git a/web/templates.go b/web/templates.go new file mode 100644 index 0000000..8e15f51 --- /dev/null +++ b/web/templates.go @@ -0,0 +1,51 @@ +package web + +import ( + "html/template" + "net/http" + + "github.com/gorilla/mux" +) + +// TmplFuncs handles global template functions +var tmplFuncs = []template.FuncMap{ + map[string]interface{}{ + "safeURL": func(s string) template.URL { + return template.URL(s) + }, + }, +} + +// AddTmplFunc adds a template function +func AddTmplFunc(name string, f interface{}) error { + tmplFuncs = append(tmplFuncs, map[string]interface{}{ + name: f, + }) + return nil +} + +// TemplateData represents object passed to template renderer +type TemplateData struct { + Env *Env `json:"-"` + RouteName string + Data map[string]interface{} +} + +// Rends a view +func (e *Env) Rends(w http.ResponseWriter, r *http.Request, template string) error { + if e.Mode == ProductionMode { + return e.Render.HTML(w, http.StatusOK, template, TemplateData{ + Env: e, + RouteName: mux.CurrentRoute(r).GetName(), + Data: GetAllData(r), + }) + } + if e.Mode == TestMode { + return e.Render.JSON(w, http.StatusOK, TemplateData{ + Env: e, + RouteName: mux.CurrentRoute(r).GetName(), + Data: GetAllData(r), + }) + } + return nil +}