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
+}