diff --git a/Makefile b/Makefile index faff1ed..77dfd66 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 watch-js watch-css watch-templates watch +.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 DB_USER=test DB_PASS=test @@ -33,7 +33,10 @@ build-go: build-prepare copy-templates: build-prepare cp -r ./src/templates/* ./build/templates -build: build-js build-css build-font copy-templates build-go +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' & @@ -44,10 +47,13 @@ watch-css: build-prepare 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-go +watch: watch-js watch-css watch-templates watch-images watch-go docker: $(DOCKER_COMPOSE) up -d @@ -61,7 +67,7 @@ migration-dev-data: docker migration-schema migration: migration-schema migration-dev-data -dev: docker migration watch +dev: docker migration build-font watch clean: -rm -r ./build diff --git a/README.md b/README.md index 30c992a..c886f12 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,16 @@ 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 ``` +## Default users + +This users are defined with this parameters: +pepper: "pepper" +cost: 10 + +Users: +* Admin user: admin / admin +* Test user: test / test + ## Run the tests diff --git a/config.yml.exemple b/config.yml.exemple index 533837f..07a4295 100644 --- a/config.yml.exemple +++ b/config.yml.exemple @@ -5,4 +5,7 @@ authorizer: 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 index 24e7778..02bb6a3 100644 --- a/fontify.json +++ b/fontify.json @@ -1,6 +1,7 @@ { "modules": [ - "bootstrap" + "bootstrap", + "font-awesome" ], "dest": "build/public" } diff --git a/package.json b/package.json index c76c19b..8772268 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "license": "ISC", "dependencies": { "bootstrap": "^3.3.6", + "font-awesome": "^4.7.0", "jquery": "^2.2.4" }, "devDependencies": { diff --git a/sql/dev/101_data.up.sql b/sql/dev/101_data.up.sql index c025886..74355ba 100644 --- a/sql/dev/101_data.up.sql +++ b/sql/dev/101_data.up.sql @@ -1,2 +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); +INSERT INTO users (name, hash, admin) VALUES ('test', '$2a$10$QHx07iyuxO1RcehgtjMgjOzv03Bx2eeSKvsxkoj9oR2NJ4cklh6ue', false); +INSERT INTO users (name, hash, admin) VALUES ('admin', '$2a$10$qAbyDZsHtcnhXhjhQZkD2uKlX72eMHsX8Hi2Cnl1vJUqHQiey2qa6', true); diff --git a/src/internal/auth/middleware.go b/src/internal/auth/middleware.go index 71af311..268ad45 100644 --- a/src/internal/auth/middleware.go +++ b/src/internal/auth/middleware.go @@ -1,23 +1,25 @@ package auth import ( + "context" "fmt" "net/http" - "github.com/gorilla/context" + "github.com/Sirupsen/logrus" ) -type key int - -const ukey key = 0 //user key - // Middleware get User from session and put it in context type Middleware struct { authorizer *Authorizer + log *logrus.Entry } -func NewMiddleware(authorizer *Authorizer) *Middleware { - return &Middleware{authorizer} +// NewMiddleware returns a new authentication middleware +func NewMiddleware(authorizer *Authorizer, log *logrus.Entry) *Middleware { + return &Middleware{ + authorizer: authorizer, + log: log.WithField("middleware", "auth"), + } } func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { @@ -25,24 +27,42 @@ func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http if err != nil { panic(err) } - context.Set(r, ukey, user) + + m.log.Debug("setting user in the context") + ctx := context.WithValue(r.Context(), "auth.user", user) + r = r.WithContext(ctx) + next(w, r) } +// MiddlewareRole handles the role checking for the current user type MiddlewareRole struct { authorizer *Authorizer + log *logrus.Entry role string loginPageGetter func() string } -func NewMiddlewareRole(authorizer *Authorizer, loginPageGetter func() string, role string) *MiddlewareRole { - return &MiddlewareRole{authorizer, role, loginPageGetter} +// NewMiddlewareRole returns a new MiddlewareRole +func NewMiddlewareRole(authorizer *Authorizer, log *logrus.Entry, loginPageGetter func() string, role string) *MiddlewareRole { + return &MiddlewareRole{ + authorizer: authorizer, + log: log.WithField("middleware", "role"), + role: role, + loginPageGetter: loginPageGetter, + } } func (m *MiddlewareRole) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - user := GetCurrentUser(r) + user := GetCurrentUser(r, m.log) if user == nil || !user.HasRole(m.role) { + if user == nil { + m.log.Debug("user is nil in the context") + } else { + m.log.Debug("user doesn't have the role") + } + cookie, err := m.authorizer.Cookiejar.Get(r, "rlogin") if err != nil { panic(err) @@ -56,9 +76,13 @@ func (m *MiddlewareRole) ServeHTTP(w http.ResponseWriter, r *http.Request, next return } + m.log.Debug("user has the role, continuing") + 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 { @@ -81,8 +105,11 @@ func GetPostLoginRedirect(a *Authorizer, w http.ResponseWriter, r *http.Request) } -func GetCurrentUser(r *http.Request) User { - u := context.Get(r, ukey) +// GetCurrentUser gets the current user from the request context +func GetCurrentUser(r *http.Request, log *logrus.Entry) User { + log.Debug("getting user from context") + + u := r.Context().Value("auth.user") if u == nil { return nil } diff --git a/src/internal/config/canape.go b/src/internal/config/canape.go index beacf47..55ff3bf 100644 --- a/src/internal/config/canape.go +++ b/src/internal/config/canape.go @@ -4,6 +4,9 @@ import ( "io/ioutil" "os" + polochon "github.com/odwrtw/polochon/lib" + "github.com/odwrtw/polochon/modules/tmdb" + "gopkg.in/yaml.v2" ) @@ -14,6 +17,10 @@ type Config struct { TraktTVClientID string `yaml:"trakttv_client_id"` TemplatesDir string `yaml:"templates_dir"` PublicDir string `yaml:"public_dir"` + + // TODO improve the detailers configurations + TmdbAPIKey string `yaml:"tmdb_api_key"` + MovieDetailers []polochon.Detailer } type AuthorizerConfig struct { @@ -42,5 +49,14 @@ func Load(path string) (*Config, error) { return nil, err } + cf.MovieDetailers = []polochon.Detailer{} + if cf.TmdbAPIKey != "" { + d, err := tmdb.New(&tmdb.Params{cf.TmdbAPIKey}) + if err != nil { + return nil, err + } + cf.MovieDetailers = append(cf.MovieDetailers, d) + } + return cf, nil } diff --git a/src/internal/movies/handlers.go b/src/internal/movies/handlers.go index 238ff8a..7bd8bcf 100644 --- a/src/internal/movies/handlers.go +++ b/src/internal/movies/handlers.go @@ -5,12 +5,9 @@ import ( "net" "net/http" "net/url" - "path/filepath" + "github.com/gorilla/mux" "github.com/odwrtw/papi" - "github.com/odwrtw/polochon/lib" - "github.com/odwrtw/polochon/modules/mock" - traktdetailer "github.com/odwrtw/polochon/modules/trakttv" "github.com/odwrtw/trakttv" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" @@ -59,8 +56,8 @@ 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) - v := auth.GetCurrentUser(r) user, ok := v.(*users.User) if !ok { return fmt.Errorf("invalid user type") @@ -79,16 +76,8 @@ func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error { return err } - //TODO use configurable detailer - // detailer, err := tmdb.New(&tmdb.Params{"57be344f84917b3f32c68a678f1482eb"}) - detailer, _ := mock.NewDetailer(nil) - if err != nil { - return err - } - for _, m := range movies { - m.Detailers = []polochon.Detailer{detailer} - err := m.GetDetails(env.Database, filepath.Join(env.Config.PublicDir, "img", "movies"), env.Log) + err := m.GetDetails(env, false) if err != nil { env.Log.Error(err) } @@ -96,10 +85,12 @@ func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error { env.Log.Info(movies) - return nil + web.SetData(r, "movies", movies) + return env.Rends(w, r, "movies/library") } func ExplorePopular(env *web.Env, w http.ResponseWriter, r *http.Request) error { + log := env.Log.WithField("function", "movies.ExplorePopular") queryOption := trakttv.QueryOption{ ExtendedInfos: []trakttv.ExtendedInfo{ @@ -112,22 +103,19 @@ func ExplorePopular(env *web.Env, w http.ResponseWriter, r *http.Request) error } trakt := trakttv.New(env.Config.TraktTVClientID) trakt.Endpoint = trakttv.ProductionEndpoint + + log.Debug("getting movies from trakttv") tmovies, err := trakt.PopularMovies(queryOption) if err != nil { return err } - - detailer, err := traktdetailer.New(&traktdetailer.Params{env.Config.TraktTVClientID}) - if err != nil { - return err - } + log.Debugf("got %d movies from trakttv", len(tmovies)) movies := []*Movie{} ids := []string{} for _, m := range tmovies { movie := New(m.IDs.ImDB) - movie.Detailers = []polochon.Detailer{detailer} - err := movie.GetDetails(env.Database, filepath.Join(env.Config.PublicDir, "img", "movies"), env.Log) + err := movie.GetDetails(env, false) if err != nil { env.Log.Error(err) continue diff --git a/src/internal/movies/movies.go b/src/internal/movies/movies.go index b1e0157..54eebd2 100644 --- a/src/internal/movies/movies.go +++ b/src/internal/movies/movies.go @@ -17,6 +17,11 @@ const ( VALUES (:imdbid, :title, :rating, :votes, :plot, :tmdbid, :year, :originaltitle, :runtime, :sorttitle, :tagline) RETURNING id;` + updateMovieQuery = ` + UPDATE movies + SET imdb_id=:imdbid, title=:title, rating=:rating, votes=:votes, plot=:plot, tmdb_id=:tmdbid, year=:year, original_title=:originaltitle, runtime=:runtime, sort_title=:sorttitle, tagline=:tagline + WHERE ID = :id;` + getMovieQueryByImdbID = ` SELECT id, imdb_id AS imdbid, title, rating, votes, plot, @@ -74,38 +79,67 @@ func (m *Movie) Get(db *sqlx.DB) error { return nil } -// GetDetails retrieves details for the movie, first try to -// get info from db, if not exists, use polochon.Detailer -// and save informations in the database for future use -func (m *Movie) GetDetails(db *sqlx.DB, imgPath string, log *logrus.Entry) error { - var err error - err = m.Get(db) - if err == nil { - // found ok - return nil +// GetDetails retrieves details for the movie, first try to get info from db, +// if not exists, use polochon.Detailer and save informations in the database +// for future use +// +// If force is used, the detailer will be used even if the movie is found in +// database +func (m *Movie) GetDetails(env *web.Env, force bool) error { + if len(m.Detailers) == 0 { + m.Detailers = env.Config.MovieDetailers } - if err != ErrNotFound { + + log := env.Log.WithFields(logrus.Fields{ + "imdb_id": m.ImdbID, + "function": "movies.GetDetails", + }) + + // If the movie is not in db, we should add it, otherwise we should update + // it + var dbFunc func(db *sqlx.DB) error + + var err error + err = m.Get(env.Database) + switch err { + case nil: + log.Debug("movie found in database") + dbFunc = m.Update + if !force { + return nil + } + case ErrNotFound: + dbFunc = m.Add + log.Debug("movie not found in database") + default: // Unexpected error return err } // so we got ErrNotFound so GetDetails from a detailer - err = m.Movie.GetDetails(log) + err = m.Movie.GetDetails(env.Log) if err != nil { return err } - err = m.Add(db) + log.Debug("got details from detailers") + + err = dbFunc(env.Database) if err != nil { return err } + log.Debug("movie added in database") + // Download poster - err = web.Download(m.Thumb, filepath.Join(imgPath, m.ImdbID+".jpg")) + imgPath := filepath.Join(env.Config.PublicDir, "img", "movies", m.ImdbID+".jpg") + err = web.Download(m.Thumb, imgPath) if err != nil { return err } + log.Debug("poster downloaded") + return nil } @@ -124,6 +158,12 @@ func (m *Movie) Add(db *sqlx.DB) error { return nil } +// Update a movie in the database +func (m *Movie) Update(db *sqlx.DB) error { + _, err := db.NamedQuery(updateMovieQuery, m) + return err +} + // Delete movie from database func (m *Movie) Delete(db *sqlx.DB) error { r, err := db.Exec(deleteMovieQuery, m.ID) diff --git a/src/internal/users/handlers.go b/src/internal/users/handlers.go index 598b748..a8fcf03 100644 --- a/src/internal/users/handlers.go +++ b/src/internal/users/handlers.go @@ -65,7 +65,7 @@ func LogoutHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { // DetailsHandler show user details func DetailsHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { - v := auth.GetCurrentUser(r) + v := auth.GetCurrentUser(r, e.Log) user, ok := v.(*User) if !ok { return fmt.Errorf("invalid user type") @@ -84,7 +84,7 @@ func DetailsHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { // EditHandler allow editing user info and configuration func EditHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { - v := auth.GetCurrentUser(r) + v := auth.GetCurrentUser(r, e.Log) user, ok := v.(*User) if !ok { return fmt.Errorf("invalid user type") diff --git a/src/internal/web/env.go b/src/internal/web/env.go index b235168..cabc99c 100644 --- a/src/internal/web/env.go +++ b/src/internal/web/env.go @@ -7,10 +7,10 @@ import ( "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" "github.com/Sirupsen/logrus" - "github.com/codegangsta/negroni" "github.com/gorilla/mux" "github.com/jmoiron/sqlx" "github.com/unrolled/render" + "github.com/urfave/negroni" ) // Env describes an environement object passed to all handlers @@ -76,7 +76,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.GetLoginRouteGetter(), role), + auth.NewMiddlewareRole(r.env.Auth, r.env.Log, r.env.GetLoginRouteGetter(), role), negroni.Wrap(handler)) r.mRoute.Handler(newHandler) return r diff --git a/src/main.go b/src/main.go index 8dfd555..3da4738 100644 --- a/src/main.go +++ b/src/main.go @@ -17,10 +17,12 @@ import ( _ "github.com/lib/pq" ) +// UserBackend represents the data backend to get the user type UserBackend struct { Database *sqlx.DB } +// Get gets the username from the UserBackend func (b *UserBackend) Get(username string) (auth.User, error) { return users.Get(b.Database, username) } @@ -32,7 +34,12 @@ func main() { cfgPath = "./config.yml" } - log := logrus.NewEntry(logrus.New()) + // Setup the logger + logger := logrus.New() + logger.Formatter = &logrus.TextFormatter{FullTimestamp: true} + logger.Level = logrus.DebugLevel + + log := logrus.NewEntry(logger) cf, err := config.Load(cfgPath) if err != nil { log.Panic(err) @@ -60,7 +67,7 @@ func main() { Config: cf, }) - authMiddleware := auth.NewMiddleware(env.Auth) + authMiddleware := auth.NewMiddleware(env.Auth, log) env.Handle("/users/login", users.LoginGETHandler).Name("users.login").Methods("GET") env.Handle("/users/login", users.LoginPOSTHandler).Name("users.login").Methods("POST") diff --git a/src/public/img/noimage.png b/src/public/img/noimage.png new file mode 100644 index 0000000..b928769 Binary files /dev/null and b/src/public/img/noimage.png differ diff --git a/src/public/less/app.less b/src/public/less/app.less index 50ebdda..85cb97c 100644 --- a/src/public/less/app.less +++ b/src/public/less/app.less @@ -1,4 +1,5 @@ @import "bootstrap/less/bootstrap.less"; +@import "font-awesome/less/font-awesome.less"; body { padding-top: 70px; @@ -9,4 +10,13 @@ body { background-color:#f1c40f; } +.movie-plot { + .text-justify; + margin-right: 5%; +} +.movie-details-buttons { + position: fixed; + bottom: 1%; + right: 1%; +} diff --git a/src/templates/movies/library.tmpl b/src/templates/movies/library.tmpl index d31395f..3005c89 100644 --- a/src/templates/movies/library.tmpl +++ b/src/templates/movies/library.tmpl @@ -1,6 +1,10 @@ {{ if $.Data.error }} + {{ if eq $.Data.error "Invalid address"}} -
+ + {{ .Runtime }} min +
+ ++ + {{ .Rating }} ({{ .Votes }} counts) +
+ +{{ .Plot }}
+ + +