diff --git a/.gitignore b/.gitignore index 6731b28..b2decc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules build config.yml +*.log diff --git a/Makefile b/Makefile index 77dfd66..08c0bdd 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: docker migration dev clean test build-prepare build-js build-css build-font build-go copy-templates copy-images watch-js watch-css watch-templates watch +.PHONY: docker migration dev clean test DB_USER=test DB_PASS=test @@ -13,48 +13,13 @@ DOCKER_COMPOSE_FILE=./docker/docker-compose.yml DOCKER_COMPOSE=docker-compose -f $(DOCKER_COMPOSE_FILE) build-prepare: - mkdir -p ./build/public/js - mkdir -p ./build/public/css - mkdir -p ./build/templates mkdir -p ./build/public/img/movies -build-js: build-prepare - node node_modules/.bin/browserify src/public/js/app.js -o build/public/js/app.js - -build-css: build-prepare - node ./node_modules/.bin/lessc --include-path=./node_modules src/public/less/app.less build/public/css/app.css - -build-font: build-prepare - node ./node_modules/.bin/fontify - -build-go: build-prepare +build: build-prepare go build -o ./build/canape src/main.go -copy-templates: build-prepare - cp -r ./src/templates/* ./build/templates - -copy-images: build-prepare - cp -r ./src/public/img/* ./build/public/img - -build: build-js build-css build-font copy-templates copy-images build-go - -watch-js: build-prepare - node ./node_modules/.bin/nodemon -e js -w src/public/js/app.js -x 'make build-js' & - -watch-css: build-prepare - node ./node_modules/.bin/nodemon -e less -w src/public/less/app.less -x 'make build-css' & - -watch-templates: build-prepare - node ./node_modules/.bin/nodemon -e tmpl -w src/templates -x 'make copy-templates' & - -watch-images: build-prepare - node ./node_modules/.bin/nodemon -e jpg,png -w src/public/img -x 'make copy-images' & - watch-go: build-prepare CONFIG_FILE="./config.yml" fresh -c fresh.conf - -watch: watch-js watch-css watch-templates watch-images watch-go - docker: $(DOCKER_COMPOSE) up -d sleep 4 @@ -67,7 +32,7 @@ migration-dev-data: docker migration-schema migration: migration-schema migration-dev-data -dev: docker migration build-font watch +dev: docker migration watch-go clean: -rm -r ./build diff --git a/README.md b/README.md index 690879b..4130bbc 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,48 @@ ## Install dependencies -Go dependancies: +### Install node + +You'll need node v6+, here's a link to install it: +https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions + +### Install yarn and dependencies + +``` +sudo npm install -g yarn +yarn install +``` + +### Go tools && Dependencies + +As there is no versioning yet, you need to manually go get all the packages + + +``` +go get ./... +``` + +go tools: ``` go get -v github.com/pilu/fresh go get -v github.com/mattes/migrate ``` -NPM dependencies: - -``` -npm install -``` - -### As there is no versioning yet, you need to manually go get all the packages - -``` -go get ./... -``` - -### NOTE: for debian users - -If you use debian the node binary is named nodejs you have to symlink it to node: - - -``` -# ln -s /usr/bin/nodejs /usr/bin/node -``` - ## Dev #### Check your config.yml file -#### Run +#### Run server + ``` make dev ``` -To setup the dev env, run server, and auto-reload on file changes +#### Run javascript tools + +``` +yarn start +``` ## Connect to the database @@ -45,6 +50,12 @@ To setup the dev env, run server, and auto-reload on file changes docker run -it --rm --link canape_postgresql_dev:postgres postgres:9.5 psql -h postgres -U test ``` +You'll need to connect to the dev database with this command: + +``` +\c dev +``` + ## Default users This users are defined with this parameters: @@ -67,17 +78,3 @@ make test ``` make clean ``` - -## Build - - -``` -make build - -``` - -## Watch js and less update, and auto-reload server - -``` -make watch -``` diff --git a/config.yml.exemple b/config.yml.exemple index 07a4295..ae281e5 100644 --- a/config.yml.exemple +++ b/config.yml.exemple @@ -1,11 +1,9 @@ authorizer: - cookie_name: auth - cookie_key: mysecretkey + secret: my_secret_app_key pepper: pepper cost: 10 pgdsn: postgres://test:test@127.0.0.1:5432/dev?sslmode=disable trakttv_client_id: my_trakttv_client_id tmdb_api_key: my_tmdb_key listen_port: 3000 -templates_dir: build/templates public_dir: build/public diff --git a/fontify.json b/fontify.json deleted file mode 100644 index 02bb6a3..0000000 --- a/fontify.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "modules": [ - "bootstrap", - "font-awesome" - ], - "dest": "build/public" -} diff --git a/gulpfile.babel.js b/gulpfile.babel.js new file mode 100644 index 0000000..2ed1f72 --- /dev/null +++ b/gulpfile.babel.js @@ -0,0 +1,65 @@ +import gulp from 'gulp'; +import babel from 'gulp-babel'; +import less from 'gulp-less'; +import del from 'del'; +import webpack from 'webpack-stream'; +import webpackConfig from './webpack.config.babel'; + +const paths = { + allSrcJs: 'src/**/*.js?(x)', + jsSrc: 'src/public/js/app.js', + jsDistDir: 'build/public/js', + lessSrc: 'src/public/less/app.less', + lessDest: 'build/public/css', + fontSrc: [ + './node_modules/font-awesome/fonts/*', + './node_modules/bootstrap/fonts/*', + ], + fontDest: 'build/public/fonts', + imgSrc: 'src/public/img/*', + imgDest: 'build/public/img/', + htmlSrc: 'src/public/index.html', + htmlDest: 'build/public/', + gulpFile: 'gulpfile.babel.js', + webpackFile: 'webpack.config.babel.js', +}; + +gulp.task('less', () => + gulp.src(paths.lessSrc) + .pipe(less({ + paths: [ './node_modules' ] + })) + .pipe(gulp.dest(paths.lessDest)) +); + +gulp.task('fonts', () => + gulp.src(paths.fontSrc) + .pipe(gulp.dest(paths.fontDest)) +); + +gulp.task('images', () => + gulp.src(paths.imgSrc) + .pipe(gulp.dest(paths.imgDest)) +); + +gulp.task('html', () => + gulp.src(paths.htmlSrc) + .pipe(gulp.dest(paths.htmlDest)) +); + +gulp.task('js', () => + gulp.src(paths.jsSrc) + .pipe(webpack(webpackConfig)) + .pipe(gulp.dest(paths.jsDistDir)) +); + +gulp.task('main', ['less', 'fonts', 'images', 'html', 'js']) + +gulp.task('watch', () => { + gulp.watch(paths.allSrcJs, ['js']); + gulp.watch(paths.lessSrc, ['less']); + gulp.watch(paths.imgSrc, ['images']); + gulp.watch(paths.htmlSrc, ['html']); +}); + +gulp.task('default', ['watch', 'main']); diff --git a/package.json b/package.json index 8772268..1b9b7ee 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,45 @@ { - "name": "canape", - "version": "0.0.1", - "description": "``` make dev ```", - "main": "index.js", - "repository": { - "type": "git", - "url": "ssh://git@gitlab.quimbo.fr:5022/odwrtw/canape-sql.git" - }, - "author": "", - "license": "ISC", - "dependencies": { - "bootstrap": "^3.3.6", - "font-awesome": "^4.7.0", - "jquery": "^2.2.4" - }, - "devDependencies": { - "browserify": "^13.0.1", - "fontify": "0.0.2", - "less": "^2.7.1", - "nodemon": "^1.9.2" - } + "name": "canape", + "scripts": { + "start": "gulp" + }, + "babel": { + "presets": [ + "react", + "latest" + ] + }, + "dependencies": { + "babel-polyfill": "^6.16.0", + "bootstrap": "^3.3.6", + "font-awesome": "^4.7.0", + "history": "^4.4.0", + "jquery": "^2.2.4", + "jwt-decode": "^2.1.0", + "react": "^15.3.2", + "react-bootstrap": "^0.30.6", + "react-dom": "^15.3.2", + "react-redux": "^4.4.6", + "react-router": "^3.0.0", + "react-router-bootstrap": "^0.23.1", + "react-router-redux": "^4.0.7", + "redux": "^3.6.0", + "redux-auth-wrapper": "^0.9.0", + "redux-logger": "^2.7.4", + "redux-thunk": "^2.1.0" + }, + "devDependencies": { + "axios": "^0.15.2", + "babel": "^6.5.2", + "babel-loader": "^6.2.7", + "babel-preset-latest": "^6.16.0", + "babel-preset-react": "^6.16.0", + "del": "^2.2.2", + "fontify": "0.0.2", + "gulp": "^3.9.1", + "gulp-babel": "^6.1.2", + "gulp-less": "^3.3.0", + "less": "^2.7.1", + "webpack-stream": "^3.2.0" + } } diff --git a/src/internal/auth/auth.go b/src/internal/auth/auth.go index 7144a68..e9457c6 100644 --- a/src/internal/auth/auth.go +++ b/src/internal/auth/auth.go @@ -3,8 +3,10 @@ package auth import ( "fmt" "net/http" + "strings" + + jwt "github.com/dgrijalva/jwt-go" - "github.com/gorilla/sessions" "golang.org/x/crypto/bcrypt" ) @@ -13,8 +15,8 @@ var ( ErrInvalidPassword = fmt.Errorf("Invalid password") // ErrInvalidSecret returned when cookie's secret is don't match ErrInvalidSecret = fmt.Errorf("Invalid secret") - // ErrCorrupted returned when session have been corrupted - ErrCorrupted = fmt.Errorf("Corrupted session") + // ErrInvalidToken returned when the jwt token is invalid + ErrInvalidToken = fmt.Errorf("Invalid token") ) // UserBackend interface for user backend @@ -27,6 +29,7 @@ type User interface { GetName() string GetHash() string HasRole(string) bool + IsAdmin() bool } // Authorizer handle sesssion @@ -36,11 +39,10 @@ type Authorizer struct { // Params for Authorizer creation type Params struct { - Backend UserBackend - Cookiejar *sessions.CookieStore - CookieName string - Pepper string - Cost int + Backend UserBackend + Pepper string + Cost int + Secret string } // New Authorizer pepper is like a salt but not stored in database, @@ -60,112 +62,56 @@ func (a *Authorizer) GenHash(password string) (string, error) { return string(b), nil } -// Login cheks password and updates cookie info -func (a *Authorizer) Login(rw http.ResponseWriter, req *http.Request, username, password string) error { - cookie, err := a.Cookiejar.Get(req, a.CookieName) - if err != nil { - return err - } - +// Login cheks password and creates a jwt token +func (a *Authorizer) Login(rw http.ResponseWriter, req *http.Request, username, password string) (User, error) { u, err := a.Backend.Get(username) if err != nil { - return err + return nil, err } + // Compare the password err = bcrypt.CompareHashAndPassword([]byte(u.GetHash()), []byte(password+a.Pepper)) if err != nil { - return ErrInvalidPassword - } - - cookie.Values["username"] = username - - // genereate secret - b, err := bcrypt.GenerateFromPassword([]byte(u.GetHash()), a.Cost) - if err != nil { - return err - } - cookie.Values["secret"] = string(b) - - err = cookie.Save(req, rw) - if err != nil { - return err - } - return nil -} - -// RegenSecret update secret in cookie with user info, usefull when updating password -func (a *Authorizer) RegenSecret(user User, w http.ResponseWriter, r *http.Request) error { - cookie, err := a.Cookiejar.Get(r, a.CookieName) - if err != nil { - return err - } - // genereate secret - b, err := bcrypt.GenerateFromPassword([]byte(user.GetHash()), a.Cost) - if err != nil { - return err - } - cookie.Values["secret"] = string(b) - - err = cookie.Save(r, w) - if err != nil { - return err - } - return nil -} - -// Logout remove cookie info -func (a *Authorizer) Logout(rw http.ResponseWriter, req *http.Request) error { - cookie, err := a.Cookiejar.Get(req, a.CookieName) - if err != nil { - return err - } - cookie.Values["username"] = nil - cookie.Values["secret"] = nil - cookie.Options.MaxAge = -1 // kill the cookie - err = cookie.Save(req, rw) - if err != nil { - return err - } - return nil -} - -// CurrentUser returns the logged in username from session -func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (User, error) { - cookie, err := a.Cookiejar.Get(req, a.CookieName) - if err != nil { - return nil, err - } - if cookie.IsNew { - return nil, nil - } - - usernameTmp := cookie.Values["username"] - if usernameTmp == nil { - return nil, nil - } - username, ok := usernameTmp.(string) - if !ok { - return nil, ErrCorrupted - } - - u, err := a.Backend.Get(username) - if err != nil { - return nil, err - } - - // Check secret - hash := u.GetHash() - secretTmp := cookie.Values["secret"] - if secretTmp == nil { - return nil, nil - } - secret, ok := secretTmp.(string) - if !ok { - return nil, ErrCorrupted - } - err = bcrypt.CompareHashAndPassword([]byte(secret), []byte(hash)) - if err != nil { - return nil, ErrInvalidSecret + return nil, ErrInvalidPassword + } + + return u, nil +} + +// CurrentUser returns the logged in username from session and verifies the token +func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (User, error) { + h := req.Header.Get("Authorization") + // No user logged + if h == "" { + return nil, nil + } + + // Get the token from the header + tokenStr := strings.Replace(h, "Bearer ", "", -1) + + // Keyfunc to decode the token + var keyfunc jwt.Keyfunc = func(token *jwt.Token) (interface{}, error) { + return []byte(a.Secret), nil + } + + var tokenClaims struct { + Username string `json:"username"` + jwt.StandardClaims + } + token, err := jwt.ParseWithClaims(tokenStr, &tokenClaims, keyfunc) + if err != nil { + return nil, err + } + + // Check the token validity + if !token.Valid { + return nil, ErrInvalidToken + } + + // Get the user + u, err := a.Backend.Get(tokenClaims.Username) + if err != nil { + return nil, err } return u, nil diff --git a/src/internal/auth/auth_test.go b/src/internal/auth/auth_test.go deleted file mode 100644 index 7a9c51c..0000000 --- a/src/internal/auth/auth_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package auth - -import ( - "fmt" - "io/ioutil" - "net/http" - "net/http/cookiejar" - "net/http/httptest" - "testing" - - "github.com/gorilla/mux" - "github.com/kr/pretty" -) - -const ( - pepper = "polp" - ckey = "plop" - cookieName = "auth" - cost = 10 - - username = "plop" - password = "ploppwd" - hash = "$2a$10$eVye8xbs6nj4TWnlTmifRuBsAU3F2hkxEcFz9WXdYjUuE6uKLVuzK" -) - -type user struct { - username string - password string - hash string -} - -func (u *user) GetHash() string { - return u.hash -} - -type Backend struct { - user *user -} - -func (b *Backend) Get(username string) (User, error) { - if username == b.user.username { - return b.user, nil - } - return nil, fmt.Errorf("invalid username") -} - -func getBackend() *Backend { - return &Backend{ - user: &user{ - username: username, - password: password, - hash: hash, - }, - } -} - -func login(w http.ResponseWriter, r *http.Request) { - a := New(getBackend(), pepper, cookieName, ckey, cost) - err := a.Login(w, r, username, password) - if err != nil { - fmt.Fprintf(w, "%s", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) - -} -func logout(w http.ResponseWriter, r *http.Request) { - a := New(getBackend(), pepper, cookieName, ckey, cost) - err := a.Logout(w, r) - if err != nil { - fmt.Fprintf(w, "%s", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) -} -func check(w http.ResponseWriter, r *http.Request) { - a := New(getBackend(), pepper, cookieName, key, cost) - u, err := a.CurrentUser(w, r) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "%s", err) - return - } - w.WriteHeader(http.StatusOK) - if u != nil { - usr, ok := u.(*user) - if !ok { - fmt.Fprintf(w, "Invalid user type") - return - } - fmt.Fprintf(w, "%s", usr.username) - } - -} - -func handlers() *mux.Router { - r := mux.NewRouter() - r.HandleFunc("/login", login).Methods("GET") - r.HandleFunc("/logout", logout).Methods("GET") - r.HandleFunc("/check", check).Methods("GET") - return r -} - -func TestAuth(t *testing.T) { - ts := httptest.NewServer(handlers()) - defer ts.Close() - - cookieJar, _ := cookiejar.New(nil) - client := &http.Client{ - Jar: cookieJar, - } - - // Check no user logged in = - res, err := client.Get(ts.URL + "/check") - if err != nil { - t.Fatal(err) - } - - body, err := ioutil.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - if res.StatusCode != http.StatusOK { - t.Fatal(body) - } - if string(body) != "" { - t.Fatalf("No user logged in expected but found: %s", body) - } - - // Login - res, err = client.Get(ts.URL + "/login") - if err != nil { - t.Fatal(err) - } - - body, err = ioutil.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - if res.StatusCode != http.StatusOK { - t.Fatal(string(body)) - } - - // Checks we are logged in - res, err = client.Get(ts.URL + "/check") - if err != nil { - t.Fatal(err) - } - - body, err = ioutil.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - if res.StatusCode != http.StatusOK { - pretty.Println(res.StatusCode) - t.Fatal(body) - } - if string(body) != username { - t.Fatalf("We expect be logged in as %s but we got: %s", username, body) - } - - // Logout - res, err = client.Get(ts.URL + "/logout") - if err != nil { - t.Fatal(err) - } - - body, err = ioutil.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - if res.StatusCode != http.StatusOK { - t.Fatal(string(body)) - } - - // Check no username logged in anymore - res, err = client.Get(ts.URL + "/check") - if err != nil { - t.Fatal(err) - } - - body, err = ioutil.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - if res.StatusCode != http.StatusOK { - t.Fatal(body) - } - if string(body) != "" { - t.Fatalf("No user logged in expected but found: %s", body) - } -} diff --git a/src/internal/auth/middleware.go b/src/internal/auth/middleware.go index e5cf922..a810288 100644 --- a/src/internal/auth/middleware.go +++ b/src/internal/auth/middleware.go @@ -2,11 +2,8 @@ package auth import ( "context" - "fmt" "net/http" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/data" - "github.com/Sirupsen/logrus" ) @@ -27,13 +24,12 @@ func NewMiddleware(authorizer *Authorizer, log *logrus.Entry) *Middleware { 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) + http.Error(w, err.Error(), http.StatusUnauthorized) + return } if user != nil { - name := user.GetName() - m.log.Debugf("setting user %s in the context", name) - data.SetData(r, "user", user) + m.log.Debugf("setting user %s in the context", user.GetName()) } else { m.log.Debugf("got a nil user in the context") } @@ -46,19 +42,17 @@ func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http // MiddlewareRole handles the role checking for the current user type MiddlewareRole struct { - authorizer *Authorizer - log *logrus.Entry - role string - loginPageGetter func() string + authorizer *Authorizer + log *logrus.Entry + role string } // NewMiddlewareRole returns a new MiddlewareRole -func NewMiddlewareRole(authorizer *Authorizer, log *logrus.Entry, loginPageGetter func() string, role string) *MiddlewareRole { +func NewMiddlewareRole(authorizer *Authorizer, log *logrus.Entry, role string) *MiddlewareRole { return &MiddlewareRole{ - authorizer: authorizer, - log: log.WithField("middleware", "role"), - role: role, - loginPageGetter: loginPageGetter, + authorizer: authorizer, + log: log.WithField("middleware", "role"), + role: role, } } @@ -72,16 +66,8 @@ func (m *MiddlewareRole) ServeHTTP(w http.ResponseWriter, r *http.Request, next m.log.Debug("user doesn't have the role") } - cookie, err := m.authorizer.Cookiejar.Get(r, "rlogin") - if err != nil { - panic(err) - } - cookie.Values["redirect"] = r.URL.Path - err = cookie.Save(r, w) - if err != nil { - panic(err) - } - http.Redirect(w, r, m.loginPageGetter(), http.StatusTemporaryRedirect) + // return unauthorized + http.Error(w, "Invalid user role", http.StatusUnauthorized) return } @@ -90,30 +76,6 @@ func (m *MiddlewareRole) ServeHTTP(w http.ResponseWriter, r *http.Request, next next(w, r) } -// GetPostLoginRedirect returns the location of the page requested before the -// users was redirected to the login page -func GetPostLoginRedirect(a *Authorizer, w http.ResponseWriter, r *http.Request) (string, error) { - cookie, err := a.Cookiejar.Get(r, "rlogin") - if err != nil { - return "", err - } - val := cookie.Values["redirect"] - if val == nil { - return "", nil - } - path, ok := val.(string) - if !ok { - return "", fmt.Errorf("invalid redirect type") - } - cookie.Values["rlogin"] = "" - err = cookie.Save(r, w) - if err != nil { - return "", err - } - return path, nil - -} - // GetCurrentUser gets the current user from the request context func GetCurrentUser(r *http.Request, log *logrus.Entry) User { log.Debug("getting user from context") diff --git a/src/internal/config/canape.go b/src/internal/config/canape.go index 55ff3bf..43dc397 100644 --- a/src/internal/config/canape.go +++ b/src/internal/config/canape.go @@ -15,7 +15,6 @@ type Config struct { PGDSN string `yaml:"pgdsn"` Port string `yaml:"listen_port"` TraktTVClientID string `yaml:"trakttv_client_id"` - TemplatesDir string `yaml:"templates_dir"` PublicDir string `yaml:"public_dir"` // TODO improve the detailers configurations @@ -24,10 +23,9 @@ type Config struct { } type AuthorizerConfig struct { - CookieName string `yaml:"cookie_name"` - Key string `yaml:"cookie_key"` - Pepper string `yaml:"pepper"` - Cost int `yaml:"cost"` + Pepper string `yaml:"pepper"` + Cost int `yaml:"cost"` + Secret string `yaml:"secret"` } func Load(path string) (*Config, error) { diff --git a/src/internal/config/polochon.go b/src/internal/config/polochon.go index 30f5644..cbc1cbf 100644 --- a/src/internal/config/polochon.go +++ b/src/internal/config/polochon.go @@ -2,6 +2,6 @@ package config // UserPolochon is polochon access parameter for a user type UserPolochon struct { - URL string - Token string + URL string `json:"url"` + Token string `json:"token"` } diff --git a/src/internal/data/data.go b/src/internal/data/data.go deleted file mode 100644 index 2a2a8cd..0000000 --- a/src/internal/data/data.go +++ /dev/null @@ -1,42 +0,0 @@ -package data - -import ( - "fmt" - "net/http" - - "context" -) - -type key int - -// dKey is key for access to response data in context -const dKey key = 0 - -// GetAllData return response's data -func GetAllData(r *http.Request) map[string]interface{} { - data := GetData(r, "data") - if data == nil { - data = make(map[string]interface{}) - } - mapData, ok := data.(map[string]interface{}) - if !ok { - fmt.Printf("something wrong with data") - } - // log.Printf("got all data %+v", mapData) - return mapData -} - -// SetData sets some response's data for access in template -func SetData(r *http.Request, key string, val interface{}) { - allData := GetAllData(r) - allData[key] = val - - ctx := context.WithValue(r.Context(), "data", allData) - *r = *r.WithContext(ctx) - allData = GetAllData(r) -} - -// GetData gets some response's data for access in template -func GetData(r *http.Request, key string) interface{} { - return r.Context().Value(key) -} diff --git a/src/internal/movies/handlers.go b/src/internal/movies/handlers.go index e08adbb..8fe2a4d 100644 --- a/src/internal/movies/handlers.go +++ b/src/internal/movies/handlers.go @@ -5,8 +5,6 @@ import ( "net" "net/http" "net/url" - "sort" - "strconv" "github.com/odwrtw/papi" polochon "github.com/odwrtw/polochon/lib" @@ -15,19 +13,13 @@ import ( "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/data" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" ) +// ErrPolochonUnavailable is an error returned if the polochon server is not available var ErrPolochonUnavailable = fmt.Errorf("Invalid polochon address") -type SortByTitle []*Movie - -func (a SortByTitle) Len() int { return len(a) } -func (a SortByTitle) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a SortByTitle) Less(i, j int) bool { return a[i].Title < a[j].Title } - func getPolochonMovies(user *users.User) ([]*Movie, error) { movies := []*Movie{} @@ -72,61 +64,14 @@ func getPolochonMovies(user *users.User) ([]*Movie, error) { func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error { v := auth.GetCurrentUser(r, env.Log) - params := struct { - Sort string - Order string - Start int - Limit int - }{ - Sort: "title", - Order: "asc", - Start: 0, - Limit: 50, - } - - err := r.ParseForm() - if err != nil { - return err - } - - if sort := r.FormValue("sort"); sort != "" { - params.Sort = sort - } - - if order := r.FormValue("order"); order != "" { - params.Order = order - } - if start := r.FormValue("start"); start != "" { - n, err := strconv.Atoi(start) - if err != nil { - return err - } - params.Start = n - } - if limit := r.FormValue("limit"); limit != "" { - n, err := strconv.Atoi(limit) - if err != nil { - return err - } - params.Limit = n - } - user, ok := v.(*users.User) if !ok { return fmt.Errorf("invalid user type") } - data.SetData(r, "user", user) movies, err := getPolochonMovies(user) - if err != nil { - // Catch network error for accessing specified polochon address - if err == ErrPolochonUnavailable { - data.SetData(r, "error", "Invalid address") - return env.Rends(w, r, "movies/library") - } - - return err + return env.RenderError(w, err) } var polochonConfig config.UserPolochon @@ -148,30 +93,10 @@ func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error { } } - var smovies sort.Interface - - switch params.Sort { - case "title": - smovies = SortByTitle(movies) - default: - return fmt.Errorf("invalid sort param") - } - - switch params.Order { - case "asc": - break - case "desc": - smovies = sort.Reverse(smovies) - default: - return fmt.Errorf("invalid order param") - } - - sort.Sort(smovies) - - data.SetData(r, "movies", movies[params.Start:params.Start+params.Limit]) - return env.Rends(w, r, "movies/library") + return env.RenderJSON(w, movies) } +// ExplorePopular returns the popular movies func ExplorePopular(env *web.Env, w http.ResponseWriter, r *http.Request) error { log := env.Log.WithField("function", "movies.ExplorePopular") @@ -206,7 +131,6 @@ func ExplorePopular(env *web.Env, w http.ResponseWriter, r *http.Request) error movies = append(movies, movie) ids = append(ids, m.IDs.ImDB) } - data.SetData(r, "movies", movies) - return env.Rends(w, r, "movies/library") + return env.RenderJSON(w, movies) } diff --git a/src/internal/users/handlers.go b/src/internal/users/handlers.go index 122a254..7f95366 100644 --- a/src/internal/users/handlers.go +++ b/src/internal/users/handlers.go @@ -1,58 +1,43 @@ package users import ( + "encoding/json" "fmt" "net/http" + "time" - "github.com/gorilla/Schema" - "github.com/kr/pretty" + jwt "github.com/dgrijalva/jwt-go" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/data" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" ) -func LoginGETHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { - return e.Rends(w, r, "users/login") -} - -func SignupGETHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { - return e.Rends(w, r, "users/signup") -} - func SignupPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { - type newForm struct { - Username string - Password string - PasswordVerify string + var data struct { + Username string `json:"username"` + Password string `json:"password"` + PasswordConfirm string `json:"password_confirm"` + } + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + return err } e.Log.Debugf("creating new user ...") - err := r.ParseForm() - if err != nil { - return err + if data.Password == "" && data.PasswordConfirm != "" { + return e.RenderError(w, fmt.Errorf("Empty password")) } - form := new(newForm) - decoder := schema.NewDecoder() - err = decoder.Decode(form, r.PostForm) - if err != nil { - return err + if data.Password != data.PasswordConfirm { + return e.RenderError(w, fmt.Errorf("Passwords missmatch")) } - user := User{ - Name: form.Username, - } - if form.Password != "" || form.PasswordVerify != "" { - if form.Password != form.PasswordVerify { - data.SetData(r, "Errors", "Password mismatch") - return e.Rends(w, r, "users/signup") - } - user.Hash, err = e.Auth.GenHash(form.Password) - if err != nil { - return err - } + user := User{Name: data.Username} + + var err error + user.Hash, err = e.Auth.GenHash(data.Password) + if err != nil { + return err } err = user.NewConfig() @@ -60,75 +45,58 @@ func SignupPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error return err } - err = user.Add(e.Database) - if err != nil { - pretty.Println(err) + if err = user.Add(e.Database); err != nil { return err } - e.Log.Debugf("new user %s created ...", form.Username) + e.Log.Debugf("new user %s created ...", data.Username) - err = e.Auth.Login(w, r, form.Username, form.Password) - if err != nil { - if err == auth.ErrInvalidPassword || err == ErrUnknownUser { - data.SetData(r, "Errors", "Error invalid user or password") - return e.Rends(w, r, "users/signup") - } - return err - } - - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return nil + return e.RenderOK(w, "User created") } +// LoginPOSTHandler handles the login proccess func LoginPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { - type loginForm struct { - Username string - Password string + var data struct { + Username string `json:"username"` + Password string `json:"password"` } - err := r.ParseForm() - if err != nil { + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 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) + user, err := e.Auth.Login(w, r, data.Username, data.Password) if err != nil { if err == auth.ErrInvalidPassword || err == ErrUnknownUser { - data.SetData(r, "Errors", "Error invalid user or password") - return e.Rends(w, r, "users/login") + return e.RenderError(w, fmt.Errorf("Error invalid user or password")) } return err } - e.Log.Debug("logged") + e.Log.Debugf("logged %s", user.GetName()) - path, err := auth.GetPostLoginRedirect(e.Auth, w, r) + // Create a jwt token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + // Not before + "nbf": time.Now().Unix(), + // Issued at + "iat": time.Now().Unix(), + // Private claims + "username": user.GetName(), + "isAdmin": user.IsAdmin(), + }) + + // Sign the token + ss, err := token.SignedString([]byte(e.Auth.Secret)) if err != nil { return err } - e.Log.Debugf("redirecting to %s", path) - if path != "" { - http.Redirect(w, r, path, http.StatusTemporaryRedirect) - return nil + var out = struct { + Token string `json:"token"` + }{ + Token: ss, } - e.Log.Debugf("got no path, redirecting to /") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return nil -} -// LogoutHandler just logout -func LogoutHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { - e.Auth.Logout(w, r) - - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return nil + return e.RenderJSON(w, out) } // DetailsHandler show user details @@ -145,8 +113,7 @@ func DetailsHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { return err } - data.SetData(r, "polochon", polochonConfig) - return e.Rends(w, r, "users/details") + return e.RenderJSON(w, polochonConfig) } // EditHandler allow editing user info and configuration @@ -156,70 +123,44 @@ func EditHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { if !ok { return fmt.Errorf("invalid user type") } - var polochonConfig config.UserPolochon - err := user.GetConfig("polochon", &polochonConfig) - if err != nil { + + var data struct { + PolochonURL string `json:"polochon_url"` + PolochonToken string `json:"polochon_token"` + Password string `json:"password"` + PasswordConfirm string `json:"password_confirm"` + } + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { return err } - if r.Method == "GET" { - data.SetData(r, "polochon", polochonConfig) - return e.Rends(w, r, "users/edit") - } - - type editForm struct { - PolochonURL string - PolochonToken string - Password string - PasswordVerify string - } - - err = r.ParseForm() - if err != nil { - return err - } - - form := new(editForm) - decoder := schema.NewDecoder() - err = decoder.Decode(form, r.PostForm) - if err != nil { - return err - } - - polochonConfig.URL = form.PolochonURL - polochonConfig.Token = form.PolochonToken - - err = user.SetConfig("polochon", polochonConfig) - if err != nil { - return err - } - - if form.Password != "" || form.PasswordVerify != "" { - if form.Password != form.PasswordVerify { - // TODO: manage form error + if data.Password == "" && data.PasswordConfirm != "" { + if data.Password != data.PasswordConfirm { + return e.RenderError(w, fmt.Errorf("Passwords empty or missmatch")) } - user.Hash, err = e.Auth.GenHash(form.Password) + + // Update the user config + var err error + user.Hash, err = e.Auth.GenHash(data.Password) if err != nil { return err } + + if err := user.Update(e.Database); err != nil { + return err + } } - err = user.Update(e.Database) - if err != nil { - pretty.Println(err) + // Update the polochon config + var polochonConfig config.UserPolochon + if err := user.GetConfig("polochon", &polochonConfig); err != nil { + return err + } + polochonConfig.URL = data.PolochonURL + polochonConfig.Token = data.PolochonToken + if err := user.SetConfig("polochon", polochonConfig); err != nil { return err } - err = e.Auth.RegenSecret(user, w, r) - if err != nil { - return err - } - - url, err := e.GetURL("users.details") - if err != nil { - return err - } - - http.Redirect(w, r, url, http.StatusTemporaryRedirect) - return nil + return e.RenderOK(w, "user updated") } diff --git a/src/internal/users/users.go b/src/internal/users/users.go index a0f036b..ad1b224 100644 --- a/src/internal/users/users.go +++ b/src/internal/users/users.go @@ -204,3 +204,7 @@ func (u *User) HasRole(role string) bool { } return true } + +func (u *User) IsAdmin() bool { + return u.HasRole(AdminRole) +} diff --git a/src/internal/web/env.go b/src/internal/web/env.go index cabc99c..3b80e54 100644 --- a/src/internal/web/env.go +++ b/src/internal/web/env.go @@ -1,8 +1,6 @@ package web import ( - "fmt" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" @@ -34,28 +32,14 @@ type EnvParams struct { // NewEnv returns a new *Env func NewEnv(p EnvParams) *Env { - e := &Env{ + return &Env{ Database: p.Database, Log: p.Log, Router: mux.NewRouter(), Auth: p.Auth, Config: p.Config, + Render: render.New(), } - - tmplFuncs = append(tmplFuncs, map[string]interface{}{ - "URL": func(name string, pairs ...string) (string, error) { - return e.GetURL(name, pairs...) - }, - }) - - e.Render = render.New(render.Options{ - Directory: p.Config.TemplatesDir, - Layout: "layout", - Funcs: tmplFuncs, - DisableHTTPErrorRendering: true, - RequirePartials: true, - }) - return e } type Route struct { @@ -76,7 +60,7 @@ func (r *Route) Methods(methods ...string) *Route { func (r *Route) WithRole(role string) *Route { handler := r.mRoute.GetHandler() newHandler := negroni.New( - auth.NewMiddlewareRole(r.env.Auth, r.env.Log, r.env.GetLoginRouteGetter(), role), + auth.NewMiddlewareRole(r.env.Auth, r.env.Log, role), negroni.Wrap(handler)) r.mRoute.Handler(newHandler) return r @@ -90,39 +74,3 @@ func (e *Env) Handle(route string, H HandlerFunc) *Route { mRoute: mRoute, } } - -// 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 -} - -// SetLoginRoute sets the route name of login page for further use -// with GetLoginRouteGetter -func (e *Env) SetLoginRoute(name string) error { - route, err := e.GetURL(name) - if err != nil { - return err - } - e.loginRoute = route - return nil -} - -// GetLoginRouteGetter allow some code parts like middleware access -// to login route name -func (e *Env) GetLoginRouteGetter() func() string { - return func() string { - if e.loginRoute == "" { - panic("Env: login route not set") - } - return e.loginRoute - } -} diff --git a/src/internal/web/render.go b/src/internal/web/render.go new file mode 100644 index 0000000..49aa863 --- /dev/null +++ b/src/internal/web/render.go @@ -0,0 +1,36 @@ +package web + +import "net/http" + +// RenderError renders an error +func (e *Env) RenderError(w http.ResponseWriter, err error) error { + return e.render(w, "error", err.Error()) +} + +// RenderOK renders a message +func (e *Env) RenderOK(w http.ResponseWriter, message string) error { + return e.render(w, "ok", message) +} + +func (e *Env) render(w http.ResponseWriter, status, message string) error { + var out = struct { + Status string `json:"status"` + Message string `json:"message"` + }{ + Status: status, + Message: message, + } + return e.Render.JSON(w, http.StatusOK, out) +} + +// RenderJSON renders the data in JSON format +func (e *Env) RenderJSON(w http.ResponseWriter, data interface{}) error { + var out = struct { + Status string `json:"status"` + Data interface{} `json:"data"` + }{ + Status: "ok", + Data: data, + } + return e.Render.JSON(w, http.StatusOK, out) +} diff --git a/src/internal/web/templates.go b/src/internal/web/templates.go deleted file mode 100644 index 7750799..0000000 --- a/src/internal/web/templates.go +++ /dev/null @@ -1,48 +0,0 @@ -package web - -import ( - "html/template" - "net/http" - - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/data" - - "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 { - Route string - Data map[string]interface{} -} - -// Rends a view -func (e *Env) Rends(w http.ResponseWriter, r *http.Request, template string) error { - if r.Header.Get("Accept") == "application/json" { - return e.Render.JSON(w, http.StatusOK, TemplateData{ - Route: mux.CurrentRoute(r).GetName(), - Data: data.GetAllData(r), - }) - } - - return e.Render.HTML(w, http.StatusOK, template, TemplateData{ - Route: mux.CurrentRoute(r).GetName(), - Data: data.GetAllData(r), - }) -} diff --git a/src/main.go b/src/main.go index 16ac02c..413482e 100644 --- a/src/main.go +++ b/src/main.go @@ -11,9 +11,9 @@ import ( "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" "github.com/Sirupsen/logrus" - "github.com/gorilla/sessions" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" + "github.com/phyber/negroni-gzip/gzip" "github.com/urfave/negroni" ) @@ -52,11 +52,10 @@ func main() { uBackend := &UserBackend{Database: db} authParams := auth.Params{ - Backend: uBackend, - Pepper: cf.Authorizer.Pepper, - CookieName: cf.Authorizer.CookieName, - Cookiejar: sessions.NewCookieStore([]byte(cf.Authorizer.Key)), - Cost: cf.Authorizer.Cost, + Backend: uBackend, + Pepper: cf.Authorizer.Pepper, + Cost: cf.Authorizer.Cost, + Secret: cf.Authorizer.Secret, } authorizer := auth.New(authParams) @@ -69,26 +68,18 @@ func main() { authMiddleware := auth.NewMiddleware(env.Auth, log) - env.Handle("/", movies.ExplorePopular).Name("movies.home") - env.Handle("/users/login", users.LoginGETHandler).Name("users.login").Methods("GET") - env.Handle("/users/login", users.LoginPOSTHandler).Name("users.login").Methods("POST") - env.Handle("/users/signup", users.SignupGETHandler).Name("users.signup").Methods("GET") - env.Handle("/users/signup", users.SignupPOSTHandler).Name("users.signup").Methods("POST") - env.Handle("/users/logout", users.LogoutHandler).Name("users.logout") - env.Handle("/users/details", users.DetailsHandler).Name("users.details").WithRole(users.UserRole) - env.Handle("/users/edit", users.EditHandler).Name("users.edit").WithRole(users.UserRole) + env.Handle("/users/login", users.LoginPOSTHandler).Methods("POST") + env.Handle("/users/signup", users.SignupPOSTHandler).Methods("POST") + env.Handle("/users/details", users.DetailsHandler).WithRole(users.UserRole) + env.Handle("/users/edit", users.EditHandler).WithRole(users.UserRole) - env.Handle("/movies/polochon", movies.FromPolochon).Name("movies.polochon").WithRole(users.UserRole) - env.Handle("/movies/explore/popular", movies.ExplorePopular).Name("movies.explore.popular").WithRole(users.UserRole) - - err = env.SetLoginRoute("users.login") - if err != nil { - log.Panic(err) - } + env.Handle("/movies/polochon", movies.FromPolochon).WithRole(users.UserRole) + env.Handle("/movies/explore/popular", movies.ExplorePopular).WithRole(users.UserRole) n := negroni.Classic() n.Use(authMiddleware) n.Use(negroni.NewStatic(http.Dir(cf.PublicDir))) + n.Use(gzip.Gzip(gzip.DefaultCompression)) n.UseHandler(env.Router) n.Run(":" + cf.Port) } diff --git a/src/templates/layout.tmpl b/src/public/index.html similarity index 59% rename from src/templates/layout.tmpl rename to src/public/index.html index f00cec8..59dc43b 100644 --- a/src/templates/layout.tmpl +++ b/src/public/index.html @@ -4,20 +4,13 @@ -