Add first users handlers

This commit is contained in:
Nicolas Duhamel 2016-02-27 21:42:43 +01:00
parent ea6d40746b
commit c184f999de
9 changed files with 519 additions and 0 deletions

1
auth/middleware.go Normal file
View File

@ -0,0 +1 @@
package auth

18
templates/layout.tmpl Normal file
View File

@ -0,0 +1,18 @@
<html>
<head>
<title>My Layout</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/vendors/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/vendors/sweetalert/lib/sweet-alert.css">
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
{{ template "navbar" $ }}
{{ yield }}
<script src="/vendors/jquery/dist/jquery.min.js"></script>
<script src="/vendors/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="/vendors/sweetalert/lib/sweet-alert.min.js"></script>
<script src="/js/app.js"></script>
</body>
</html>

161
templates/navbar.tmpl Normal file
View File

@ -0,0 +1,161 @@
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Canapé</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
{{ if .Data.user.IsLogged }}
<li {{ if eq .Name "MoviesLibrary" }} class="active" {{ end }}><a href="{{ .Env.GetURL "MoviesLibrary" }}">Movies</a></li>
<li {{ if eq .Name "TVShowsLibrary" }} class="active" {{ end }}><a href="{{ .Env.GetURL "TVShowsLibrary" }}">Shows</a></li>
{{ end }}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-list"></i>
Explore
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<li>
<a href="{{.Env.GetURL "TVShowsExplore" }}">
TVShows
</a>
</li>
<li>
<a href="{{ .Env.GetURL "MoviesExplore" "sort" "seeds" }}">
Movies
</a>
</li>
</ul>
</li>
{{ if .Data.user.IsLogged }}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-list"></i>
My Watchlist
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<li>
<a href="{{.Env.GetURL "TVShowsWichlist" }}">
TVShows
</a>
</li>
<li>
<a href="{{ .Env.GetURL "MoviesWichlist" }}">
Movies
</a>
</li>
</ul>
</li>
{{ end }}
</ul>
{{ if .Data.user.Username }}
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-user"></i>
{{ .Data.user.Username }}
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<li>
<a href="{{ .Env.GetURL "UsersDetails" }}" rel="nofollow">
<i class="fa fa-user"></i> My Account
</a>
</li>
<li>
<a data-method="delete" href="{{ .Env.GetURL "logout" }}" rel="nofollow">
<i class="fa fa-sign-out"></i> Sign out
</a>
</li>
</ul>
</li>
</ul>
{{ else }}
<ul class="nav navbar-nav navbar-right">
<li>
<a href="{{ .Env.GetURL "login" }}">Sign in</a>
</li>
</ul>
{{ end }}
{{ if or (eq $.Name "MoviesLibrary") (eq $.Name "MoviesExplore") }}
<div class="hidden-sm hidden-md">
<form class="navbar-form navbar-right" role="search" action="{{ $.Env.GetURL "MoviesSearch" }}" method="get">
<div class="form-group">
<input type="text" class="form-control" size="13" placeholder="search movies" name="keyword" /><br/>
</div>
<button type="submit" class="btn btn-default">search</button>
</form>
</div>
<div class="visible-sm visible-md">
<ul class="nav navbar-nav navbar-right"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-search"></i>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<form class="navbar-form" role="search" action="{{ $.Env.GetURL "MoviesSearch" }}" method="get">
<li>
<input type="text" class="form-control" size="13" placeholder="search movies" name="keyword" /><br/>
</li>
<button type="submit" class="btn btn-default col-xs-12">search</button>
</form>
</ul>
</ul>
</div>
{{ end }}
{{ if or (eq $.Name "TVShowsLibrary") (eq $.Name "TVShowsExplore") }}
<div class="hidden-sm hidden-md">
<form class="navbar-form navbar-right" role="search" action="{{ $.Env.GetURL "TVShowsSearch" }}" method="GET">
<div class="form-group">
<input type="text" class="form-control" size="13" placeholder="Search TV Shows" name="keyword" /><br/>
</div>
<button type="submit" class="btn btn-default">Search</button>
</form>
</div>
<div class="visible-sm visible-md">
<ul class="nav navbar-nav navbar-right"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-search"></i>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<form class="navbar-form" role="search" action="{{ $.Env.GetURL "TVShowsSearch" }}" method="GET">
<li>
<input type="text" class="form-control" size="13" placeholder="Search TV Shows" name="keyword" /><br/>
</li>
<button type="submit" class="btn btn-default col-xs-12">Search</button>
</form>
</ul>
</ul>
</div>
{{ end }}
</div>
</div>
</nav>

View File

@ -0,0 +1 @@
hello wolrd

100
users/handlers.go Normal file
View File

@ -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")
}

59
users/handles_test.go Normal file
View File

@ -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))
}
})
}

36
web/data.go Normal file
View File

@ -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)
}

92
web/env.go Normal file
View File

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

51
web/templates.go Normal file
View File

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