Make the server act like an API
This commit is contained in:
parent
466c3f4295
commit
06837a8776
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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) {
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -15,13 +15,14 @@ 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")
|
||||
|
||||
// SortByTitle helps sort the movies
|
||||
type SortByTitle []*Movie
|
||||
|
||||
func (a SortByTitle) Len() int { return len(a) }
|
||||
@ -115,18 +116,10 @@ func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error {
|
||||
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
|
||||
@ -168,10 +161,10 @@ func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error {
|
||||
|
||||
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 +199,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)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -204,3 +204,7 @@ func (u *User) HasRole(role string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *User) IsAdmin() bool {
|
||||
return u.HasRole(AdminRole)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -1,48 +1,36 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
import "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)
|
||||
},
|
||||
},
|
||||
// RenderError renders an error
|
||||
func (e *Env) RenderError(w http.ResponseWriter, err error) error {
|
||||
return e.render(w, "error", err.Error())
|
||||
}
|
||||
|
||||
// AddTmplFunc adds a template function
|
||||
func AddTmplFunc(name string, f interface{}) error {
|
||||
tmplFuncs = append(tmplFuncs, map[string]interface{}{
|
||||
name: f,
|
||||
})
|
||||
return nil
|
||||
// RenderOK renders a message
|
||||
func (e *Env) RenderOK(w http.ResponseWriter, message string) error {
|
||||
return e.render(w, "ok", message)
|
||||
}
|
||||
|
||||
// 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),
|
||||
})
|
||||
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.HTML(w, http.StatusOK, template, TemplateData{
|
||||
Route: mux.CurrentRoute(r).GetName(),
|
||||
Data: data.GetAllData(r),
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
@ -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),
|
||||
})
|
||||
}
|
31
src/main.go
31
src/main.go
@ -11,7 +11,6 @@ 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/urfave/negroni"
|
||||
@ -52,11 +51,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,22 +67,13 @@ 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)
|
||||
|
@ -1,9 +0,0 @@
|
||||
{{ if $.Data.Errors }}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3 col-xs-12">
|
||||
<div class="alert alert-warning">
|
||||
<p>{{ $.Data.Errors }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Canape</title>
|
||||
|
||||
<link href="/css/app.css" rel="stylesheet">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
{{ template "errors" $ }}
|
||||
{{ template "success" $ }}
|
||||
{{ template "navbar" $ }}
|
||||
<div class="container-fluid">
|
||||
<div class="container">
|
||||
{{ yield }}
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,61 +0,0 @@
|
||||
{{ if $.Data.error }}
|
||||
|
||||
{{ if eq $.Data.error "Invalid address"}}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
The polochon API adress specified in your configuration is invalid or unreachable,
|
||||
<a href="{{ URL "users.edit"}}">change it</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ else }}
|
||||
|
||||
<div class="row" id="movie-library">
|
||||
|
||||
<div class="col-xs-5 col-md-8">
|
||||
<div class="row">
|
||||
{{ range $.Data.movies }}
|
||||
<div class="col-xs-12 col-md-3">
|
||||
<a href="#" class="thumbnail" data-imdbid="{{.ImdbID}}">
|
||||
<img src="/img/movies/{{.ImdbID}}.jpg" onerror="this.onerror=null;this.src='/img/noimage.png';">
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-7 col-md-4">
|
||||
{{ range $.Data.movies}}
|
||||
<div id="{{.ImdbID}}-detail" class="hidden movie-detail affix">
|
||||
<h1 class="hidden-xs">{{ .Title }}</h1>
|
||||
<h3 class="visible-xs">{{ .Title }}</h3>
|
||||
<h4 class="hidden-xs">{{ .Year }}</h4>
|
||||
|
||||
<p>
|
||||
<i class="fa fa-clock-o"></i>
|
||||
{{ .Runtime }} min
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<i class="fa fa-star-o"></i>
|
||||
{{ .Rating }} <small>({{ .Votes }} counts)</small>
|
||||
</p>
|
||||
|
||||
<p class="movie-plot">{{ .Plot }}</p>
|
||||
|
||||
<div class="movie-details-buttons">
|
||||
{{ if .PolochonURL }}
|
||||
<a type="button" class="btn btn-success btn-sm" href="{{ .PolochonURL }}">
|
||||
<i class="fa fa-download"></i> Download
|
||||
</a>
|
||||
{{ end }}
|
||||
<a id="imdb-link" type="button" class="btn btn-sm btn-warning" href="http://www.imdb.com/title/{{ .ImdbID }}">
|
||||
<i class="fa fa-external-link"></i> IMDB
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{ end }}
|
@ -1,71 +0,0 @@
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="container">
|
||||
<div class="container-fluid">
|
||||
<!-- Brand and toggle get grouped for better mobile display -->
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
|
||||
<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="#">Canape</a>
|
||||
</div>
|
||||
|
||||
<!-- Collect the nav links, forms, and other content for toggling -->
|
||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a href="{{ URL "movies.explore.popular"}}">Movies</a></li>
|
||||
<li><a href="#">TV Shows</a></li>
|
||||
{{ if $.Data.user }}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-list"></i>
|
||||
My Library
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li {{ if eq $.Route "movies.polochon" }}class="active"{{ end }}>
|
||||
<a href="{{ URL "movies.polochon" }}?limit=10">Movies{{ if eq $.Route "movies.polochon" }}<span class="sr-only">Movies</span>{{ end }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">
|
||||
TVShows
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{{ if $.Data.user }}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{{$.Data.user.Name}} <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="{{ URL "users.details"}}">My account</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ URL "users.edit"}}">Edit</a>
|
||||
</li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li>
|
||||
<a href="{{ URL "users.logout"}}">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{{ else }}
|
||||
<li>
|
||||
<a href="{{ URL "users.login"}}">Sign in</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ URL "users.signup"}}">Sign up</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
</div><!-- /.navbar-collapse -->
|
||||
</div><!-- /.container-fluid -->
|
||||
</div><!-- /.container -->
|
||||
</nav>
|
@ -1,9 +0,0 @@
|
||||
{{ if $.Data.Success }}
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="alert alert-success">
|
||||
{{ $.Data.Success }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
@ -1,26 +0,0 @@
|
||||
<div class="container">
|
||||
<div id="user-details" class="content-fluid">
|
||||
|
||||
<div class="col-md-6 col-md-offset-3 col-xs-12">
|
||||
<h2>{{ $.Data.user.Name }}'s informations</h2>
|
||||
<hr>
|
||||
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
Polochon URL
|
||||
</div>
|
||||
<div class="panel-body">{{ $.Data.polochon.URL }}</div>
|
||||
</div>
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
Polochon token
|
||||
</div>
|
||||
<div class="panel-body">{{ $.Data.polochon.Token }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a class="btn btn-default pull-left" href="{{ URL "users.edit" }}">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,40 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="content-fluid">
|
||||
<div class="col-md-6 col-md-offset-3 col-xs-12">
|
||||
<h2>Edit user</h2>
|
||||
<hr>
|
||||
|
||||
<form accept-charset="UTF-8" action="{{ URL "users.edit" }}" method="POST" class="form-horizontal" id="user">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="PolochonURL">Polochon URL</label>
|
||||
<input autofocus="autofocus" class="form-control" name="PolochonURL" type="text" value="{{ $.Data.polochon.URL }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="PolochonToken">Polochon token</label>
|
||||
<input autofocus="autofocus" class="form-control" name="PolochonToken" type="text" value="{{ $.Data.polochon.Token }}" >
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="Password">Password</label>
|
||||
<input autocomplete="off" class="form-control" name="Password" type="password" value="">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="PasswordVerify">Confirm Password</label>
|
||||
<input autocomplete="off" class="form-control" name="PasswordVerify" type="password" value="">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input class="btn btn-primary pull-right" type="submit" value="Update">
|
||||
<a class="btn btn-default pull-left" href="{{ URL "users.details" }}">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,27 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="content-fluid">
|
||||
<div class="col-md-6 col-md-offset-3 col-xs-12">
|
||||
<h2>Log in</h2>
|
||||
<hr>
|
||||
<form accept-charset="UTF-8" action="/users/login" method="POST" class="form-horizontal">
|
||||
<div>
|
||||
<label for="user_email">Username</label>
|
||||
<br>
|
||||
<input autofocus="autofocus" class="form-control" id="username" name="Username" type="username" value="">
|
||||
<p></p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="user_password">Password</label>
|
||||
<br>
|
||||
<input autocomplete="off" class="form-control" id="password" name="Password" type="password">
|
||||
<p></p>
|
||||
</div>
|
||||
<div>
|
||||
<input class="btn btn-primary pull-right" type="submit" value="Log in">
|
||||
<br>
|
||||
</div>
|
||||
<a class="btn btn-default btn-sm pull-left" href="/movies/%3cnil%3e">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,39 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="content-fluid">
|
||||
<div class="col-md-6 col-md-offset-3 col-xs-12">
|
||||
<h2>Sign up</h2>
|
||||
<hr>
|
||||
<form accept-charset="UTF-8" action="{{ URL "users.signup" }}" method="POST" class="form-horizontal" id="new_user">
|
||||
<div>
|
||||
<p class="">
|
||||
<label for="user_email">Username</label>
|
||||
<br>
|
||||
<input autofocus="autofocus" class="form-control" id="username" name="Username" type="username" value="">
|
||||
<span class="error"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="">
|
||||
<label for="user_password">Password</label>
|
||||
<br>
|
||||
<input autocomplete="off" class="form-control" id="password" name="Password" type="password" value="">
|
||||
<span class="error"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="">
|
||||
<label for="user_password">Confirm Password</label>
|
||||
<br>
|
||||
<input autocomplete="off" class="form-control" id="passwordVerify" name="PasswordVerify" type="password" value="">
|
||||
<span class="error"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<input class="btn btn-primary pull-right" type="submit" value="Sign up">
|
||||
<br>
|
||||
</div>
|
||||
<a class="btn btn-default btn-sm pull-left" href="/movies/%3cnil%3e">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Loading…
x
Reference in New Issue
Block a user