Add Env.Handle and Env.HandleRole functions

This commit is contained in:
Nicolas Duhamel 2016-04-25 22:55:10 +02:00
parent 054f5103e6
commit 5778fd45df
13 changed files with 149 additions and 263 deletions

View File

@ -25,6 +25,7 @@ type UserBackend interface {
// User interface for user // User interface for user
type User interface { type User interface {
GetHash() string GetHash() string
HasRole(string) bool
} }
// Authorizer handle sesssion // Authorizer handle sesssion

61
auth/middleware.go Normal file
View File

@ -0,0 +1,61 @@
package auth
import (
"net/http"
"github.com/gorilla/context"
)
type key int
const ukey key = 0
// AuthMiddleware get User from session and put it in context
type Middleware struct {
authorizer *Authorizer
}
func NewMiddleware(authorizer *Authorizer) *Middleware {
return &Middleware{authorizer}
}
func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
user, err := m.authorizer.CurrentUser(w, r)
if err != nil {
panic(err)
}
context.Set(r, ukey, user)
next(w, r)
}
type MiddlewareRole struct {
authorizer *Authorizer
role string
}
func NewMiddlewareRole(authorizer *Authorizer, role string) *MiddlewareRole {
return &MiddlewareRole{authorizer, role}
}
func (m *MiddlewareRole) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
user := GetCurrentUser(r)
if user == nil || !user.HasRole(m.role) {
//TODO: redirect to login page and save wanted page
return
}
next(w, r)
}
func GetCurrentUser(r *http.Request) User {
u := context.Get(r, ukey)
if u == nil {
return nil
}
user, ok := u.(User)
if !ok {
panic("Invalid user type")
}
return user
}

13
main.go
View File

@ -10,7 +10,6 @@ import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/codegangsta/negroni" "github.com/codegangsta/negroni"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/lib/pq" _ "github.com/lib/pq"
) )
@ -35,13 +34,17 @@ func main() {
uBackend := &UserBackend{db} uBackend := &UserBackend{db}
authorizer := auth.New(uBackend, "peeper", "cookieName", "cookieKey", 10) authorizer := auth.New(uBackend, "peeper", "cookieName", "cookieKey", 10)
env := web.NewEnv(db, authorizer, log, "/templates") env := web.NewEnv(db, authorizer, log, "./templates")
authMiddleware := auth.NewMiddleware(env.Auth)
router := mux.NewRouter() env.Handle("users.login", "/users/login", users.LoginHandler)
env.Handle("users.logout", "users/logout", users.LogoutHandler)
env.HandleRole("users.details", "/users/details", users.DetailsHandler, users.UserRole)
router.Handle("/", env.Handler(movies.PolochonMovies)) env.HandleRole("movies.polochon", "/", movies.PolochonMovies, users.UserRole)
n := negroni.Classic() n := negroni.Classic()
n.UseHandler(router) n.Use(authMiddleware)
n.UseHandler(env.Router)
n.Run(":3000") n.Run(":3000")
} }

View File

@ -4,7 +4,7 @@ import (
"net/http" "net/http"
"github.com/odwrtw/polochon/lib" "github.com/odwrtw/polochon/lib"
"github.com/odwrtw/polochon/modules/tmdb" "github.com/odwrtw/polochon/modules/mock"
"gitlab.quimbo.fr/odwrtw/canape-sql/web" "gitlab.quimbo.fr/odwrtw/canape-sql/web"
"gitlab.quimbo.fr/odwrtw/papi" "gitlab.quimbo.fr/odwrtw/papi"
@ -27,7 +27,8 @@ func PolochonMovies(env *web.Env, w http.ResponseWriter, r *http.Request) error
movies := []*Movie{} movies := []*Movie{}
//TODO use configurable detailer //TODO use configurable detailer
detailer, err := tmdb.New(&tmdb.Params{"57be344f84917b3f32c68a678f1482eb"}) // detailer, err := tmdb.New(&tmdb.Params{"57be344f84917b3f32c68a678f1482eb"})
detailer, _ := mock.NewDetailer(nil)
if err != nil { if err != nil {
return err return err
} }

View File

@ -17,6 +17,7 @@ CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE, name text NOT NULL UNIQUE,
hash text NOT NULL, hash text NOT NULL,
admin boolean,
LIKE base INCLUDING DEFAULTS LIKE base INCLUDING DEFAULTS
); );
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();

View File

@ -0,0 +1 @@
DELETE FROM users;

2
sqltest/001_data.up.sql Normal file
View File

@ -0,0 +1,2 @@
INSERT INTO users (name, hash, admin) VALUES ('test', '$2a$10$DPsyngE6ccXzzE38.JJv3OIpvU/lSjfMyg9CR68F8h6krKIyVJYrW', false);
INSERT INTO users (name, hash, admin) VALUES ('admin', '$2a$10$e3564lLAh.0tIHQu8kfzsunViwd56AvGPeUypuCUcE3Vh09RBZci.', true);

View File

@ -3,16 +3,8 @@
<title>My Layout</title> <title>My Layout</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <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> </head>
<body> <body>
{{ template "navbar" $ }}
{{ yield }} {{ 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> </body>
</html> </html>

View File

@ -1,161 +0,0 @@
<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

@ -1 +1,5 @@
hello wolrd <form action="/users/login" method="post">
<input type="text" name="Username">
<input type="text" name="Password">
<input type="submit" value="Submit">
</form>

View File

@ -4,85 +4,55 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/Schema"
"github.com/mholt/binding"
"gitlab.quimbo.fr/odwrtw/canape-sql/auth" "gitlab.quimbo.fr/odwrtw/canape-sql/auth"
"gitlab.quimbo.fr/odwrtw/canape-sql/web" "gitlab.quimbo.fr/odwrtw/canape-sql/web"
) )
// FormErrors map[field name]message func LoginHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
type FormErrors map[string]string if r.Method == "GET" {
// 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") return e.Rends(w, r, "users/login")
} }
err := e.Auth.Login(w, r, form.Username, form.Password) type loginForm struct {
if err != nil { Username string
if err == auth.ErrInvalidPassword || err == ErrUnknownUser { Password string
web.SetData(r, "FormErrors", FormErrors{"password": "Invalid username or password"})
return e.Rends(w, r, "users/login")
}
return web.InternalError(err)
} }
return nil err := r.ParseForm()
}
func userDetailsHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
u, err := e.Auth.CurrentUser(w, r)
if err != nil { if err != nil {
return err return err
} }
form := new(loginForm)
decoder := schema.NewDecoder()
err = decoder.Decode(form, r.PostForm)
if err != nil {
return err
}
err = e.Auth.Login(w, r, form.Username, form.Password)
if err != nil {
if err == auth.ErrInvalidPassword || err == ErrUnknownUser {
web.SetData(r, "FormErrors", "Error invalid user or password")
return e.Rends(w, r, "users/login")
}
return err
}
//TODO: redirect to user details or to previous location
return nil
}
func LogoutHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
e.Auth.Logout(w, r)
//TODO: redirect to login page
return nil
}
func DetailsHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
u := auth.GetCurrentUser(r)
if u == nil { if u == nil {
return nil return nil
} }
@ -92,9 +62,3 @@ func userDetailsHandler(e *web.Env, w http.ResponseWriter, r *http.Request) erro
} }
return nil 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")
}

View File

@ -9,9 +9,9 @@ import (
) )
const ( const (
addUserQuery = `INSERT INTO users (name, hash) VALUES ($1, $2) RETURNING id;` addUserQuery = `INSERT INTO users (name, hash, admin) VALUES ($1, $2, $3) RETURNING id;`
getUserQuery = `SELECT * FROM users WHERE name=$1;` getUserQuery = `SELECT * FROM users WHERE name=$1;`
updateUserQuery = `UPDATE users SET name=:name, hash=:hash RETURNING *;` updateUserQuery = `UPDATE users SET name=:name, hash=:hash, admin=:admin RETURNING *;`
deleteUseQuery = `DELETE FROM users WHERE id=:id;` deleteUseQuery = `DELETE FROM users WHERE id=:id;`
addTokenQuery = `INSERT INTO tokens (value, user_id) VALUES ($1, $2) RETURNING id;` addTokenQuery = `INSERT INTO tokens (value, user_id) VALUES ($1, $2) RETURNING id;`
@ -20,14 +20,20 @@ const (
deleteTokenQuery = `DELETE FROM tokens WHERE user_id=$1 AND value=$2;` deleteTokenQuery = `DELETE FROM tokens WHERE user_id=$1 AND value=$2;`
) )
const (
UserRole = "user"
AdminRole = "admin"
)
// ErrUnknownUser returned web a user does'nt exist // ErrUnknownUser returned web a user does'nt exist
var ErrUnknownUser = fmt.Errorf("users: user does'nt exist") var ErrUnknownUser = fmt.Errorf("users: user does'nt exist")
// User represents an user // User represents an user
type User struct { type User struct {
sqly.BaseModel sqly.BaseModel
Name string Name string
Hash string Hash string
Admin bool
} }
// Token represents a token // Token represents a token
@ -52,7 +58,7 @@ func Get(q sqlx.Queryer, name string) (*User, error) {
// Add user to database or raises an error // Add user to database or raises an error
func (u *User) Add(q sqlx.Queryer) error { func (u *User) Add(q sqlx.Queryer) error {
var id string var id string
err := q.QueryRowx(addUserQuery, u.Name, u.Hash).Scan(&id) err := q.QueryRowx(addUserQuery, u.Name, u.Hash, u.Admin).Scan(&id)
if err != nil { if err != nil {
return err return err
} }
@ -136,3 +142,10 @@ func (u *User) DeleteToken(ex *sqlx.DB, value string) error {
func (u *User) GetHash() string { func (u *User) GetHash() string {
return u.Hash return u.Hash
} }
func (u *User) HasRole(role string) bool {
if role == AdminRole && !u.Admin {
return false
}
return true
}

View File

@ -7,6 +7,7 @@ import (
"gitlab.quimbo.fr/odwrtw/canape-sql/auth" "gitlab.quimbo.fr/odwrtw/canape-sql/auth"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/codegangsta/negroni"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/unrolled/render" "github.com/unrolled/render"
@ -62,9 +63,17 @@ func (e *Env) GetURL(name string, pairs ...string) (string, error) {
return URL.String(), nil return URL.String(), nil
} }
// Handler create a new handler type HandlerFunc func(e *Env, w http.ResponseWriter, r *http.Request) error
func (e *Env) Handler(H func(e *Env, w http.ResponseWriter, r *http.Request) error) Handler {
return Handler{e, H} func (e *Env) Handle(name, route string, H HandlerFunc) {
e.Router.Handle(route, Handler{e, H})
}
func (e *Env) HandleRole(name, route string, H HandlerFunc, role string) {
e.Router.Handle(route, negroni.New(
auth.NewMiddlewareRole(e.Auth, role),
negroni.Wrap(Handler{e, H}),
))
} }
// The Handler struct that takes a configured Env and a function matching our // The Handler struct that takes a configured Env and a function matching our
@ -85,8 +94,3 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StatusInternalServerError) http.StatusInternalServerError)
} }
} }
// InternalError create a internal error
func InternalError(err error) error {
return nil
}