From 5e81b17e2862c806c0a21b7552b55ea4452f1176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Sun, 18 Feb 2018 16:57:16 +0100 Subject: [PATCH 1/4] Add user token validation --- backend/auth/auth.go | 59 +++++++++++++++--- backend/auth/middleware.go | 23 +++---- backend/main.go | 2 +- backend/routes.go | 3 + backend/tokens/tokens.go | 96 +++++++++++++++++++++++++++++ backend/users/handlers.go | 94 +++++++++++++++++++++------- backend/users/users.go | 63 ------------------- migrations/0006_user_token.down.sql | 1 + migrations/0006_user_token.up.sql | 12 ++++ 9 files changed, 248 insertions(+), 105 deletions(-) create mode 100644 backend/tokens/tokens.go create mode 100644 migrations/0006_user_token.down.sql create mode 100644 migrations/0006_user_token.up.sql diff --git a/backend/auth/auth.go b/backend/auth/auth.go index 0fefd82..6b3c331 100644 --- a/backend/auth/auth.go +++ b/backend/auth/auth.go @@ -4,8 +4,11 @@ import ( "fmt" "net/http" "strings" + "time" jwt "github.com/dgrijalva/jwt-go" + "github.com/jmoiron/sqlx" + "gitlab.quimbo.fr/odwrtw/canape/backend/tokens" "golang.org/x/crypto/bcrypt" ) @@ -17,6 +20,8 @@ var ( ErrInvalidSecret = fmt.Errorf("Invalid secret") // ErrInvalidToken returned when the jwt token is invalid ErrInvalidToken = fmt.Errorf("Invalid token") + // ErrUnauthenticatedUser returned when a user is not authenticated + ErrUnauthenticatedUser = fmt.Errorf("Unauthenticated user") ) // UserBackend interface for user backend @@ -35,6 +40,7 @@ type User interface { // Authorizer handle sesssion type Authorizer struct { + db *sqlx.DB Params } @@ -48,8 +54,9 @@ type Params struct { // New Authorizer pepper is like a salt but not stored in database, // cost is the bcrypt cost for hashing the password -func New(params Params) *Authorizer { +func New(db *sqlx.DB, params Params) *Authorizer { return &Authorizer{ + db: db, Params: params, } } @@ -64,7 +71,7 @@ func (a *Authorizer) GenHash(password string) (string, error) { } // Login cheks password and creates a jwt token -func (a *Authorizer) Login(rw http.ResponseWriter, req *http.Request, username, password string) (User, error) { +func (a *Authorizer) Login(username, password string) (*tokens.Token, error) { u, err := a.Backend.GetUser(username) if err != nil { return nil, err @@ -76,7 +83,34 @@ func (a *Authorizer) Login(rw http.ResponseWriter, req *http.Request, username, return nil, ErrInvalidPassword } - return u, nil + // Create a jwt token + jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + // Not before + "nbf": time.Now().Unix(), + // Issued at + "iat": time.Now().Unix(), + // Private claims + "username": u.GetName(), + "isAdmin": u.IsAdmin(), + "isActivated": u.IsActivated(), + }) + + // Sign the token + ss, err := jwtToken.SignedString([]byte(a.Secret)) + if err != nil { + return nil, err + } + + t := &tokens.Token{ + Token: ss, + Username: u.GetName(), + } + + if err := t.Add(a.db); err != nil { + return nil, err + } + + return t, nil } // CurrentUser returns the logged in username from session and verifies the token @@ -92,14 +126,14 @@ func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (Use if tokenStr == "" { tokenCookie, err := req.Cookie("token") if err != nil || tokenCookie == nil { - return nil, nil + return nil, ErrUnauthenticatedUser } tokenStr = tokenCookie.Value } // No user logged if tokenStr == "" { - return nil, nil + return nil, ErrUnauthenticatedUser } // Keyfunc to decode the token @@ -111,13 +145,13 @@ func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (Use Username string `json:"username"` jwt.StandardClaims } - token, err := jwt.ParseWithClaims(tokenStr, &tokenClaims, keyfunc) + jwtToken, err := jwt.ParseWithClaims(tokenStr, &tokenClaims, keyfunc) if err != nil { return nil, err } // Check the token validity - if !token.Valid { + if !jwtToken.Valid { return nil, ErrInvalidToken } @@ -127,5 +161,16 @@ func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (Use return nil, err } + // Check the token in database + token, err := tokens.GetUserToken(a.db, u.GetName(), tokenStr) + if err != nil { + return nil, ErrInvalidToken + } + + token.UserAgent = req.UserAgent() + if err := token.Update(a.db); err != nil { + return nil, err + } + return u, nil } diff --git a/backend/auth/middleware.go b/backend/auth/middleware.go index 1ae088f..c4c9f35 100644 --- a/backend/auth/middleware.go +++ b/backend/auth/middleware.go @@ -25,21 +25,22 @@ 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 { + switch err { + case nil: + m.log.Debugf("setting user %s in the context", user.GetName()) + ctxKey := authContextKey("auth.user") + ctx := context.WithValue(r.Context(), ctxKey, user) + r = r.WithContext(ctx) + case ErrUnauthenticatedUser: + m.log.Debugf("unauthenticated user") + case ErrInvalidToken: + m.log.Debugf("user has an invalid token") + default: + m.log.Error(err) http.Error(w, err.Error(), http.StatusUnauthorized) return } - if user != nil { - m.log.Debugf("setting user %s in the context", user.GetName()) - } else { - m.log.Debugf("got a nil user in the context") - } - - ctxKey := authContextKey("auth.user") - ctx := context.WithValue(r.Context(), ctxKey, user) - r = r.WithContext(ctx) - next(w, r) } diff --git a/backend/main.go b/backend/main.go index 9e1ab1c..19b0416 100644 --- a/backend/main.go +++ b/backend/main.go @@ -51,7 +51,7 @@ func main() { Cost: cf.Authorizer.Cost, Secret: cf.Authorizer.Secret, } - authorizer := auth.New(authParams) + authorizer := auth.New(db, authParams) // Create web environment needed by the app env := web.NewEnv(web.EnvParams{ diff --git a/backend/routes.go b/backend/routes.go index e972443..303a210 100644 --- a/backend/routes.go +++ b/backend/routes.go @@ -16,6 +16,9 @@ func setupRoutes(env *web.Env) { env.Handle("/users/signup", users.SignupPOSTHandler).Methods("POST") env.Handle("/users/details", users.DetailsHandler).WithRole(users.UserRole).Methods("GET") env.Handle("/users/edit", users.EditHandler).WithRole(users.UserRole).Methods("POST") + env.Handle("/users/tokens", users.GetTokensHandler).WithRole(users.UserRole).Methods("GET") + env.Handle("/users/tokens/{token}", users.EditTokenHandler).WithRole(users.UserRole).Methods("POST") + env.Handle("/users/tokens/{token}", users.DeleteTokenHandler).WithRole(users.UserRole).Methods("DELETE") // Movies routes env.Handle("/movies/polochon", movies.PolochonMoviesHandler).WithRole(users.UserRole).Methods("GET") diff --git a/backend/tokens/tokens.go b/backend/tokens/tokens.go new file mode 100644 index 0000000..d25f24a --- /dev/null +++ b/backend/tokens/tokens.go @@ -0,0 +1,96 @@ +package tokens + +import ( + "database/sql" + "errors" + "fmt" + + "github.com/jmoiron/sqlx" + "gitlab.quimbo.fr/odwrtw/canape/backend/sqly" +) + +const ( + addTokenQuery = `INSERT INTO tokens (token, username) VALUES ($1, $2);` + getTokenQuery = `SELECT * FROM tokens WHERE token=$1;` + getUserTokenQuery = `SELECT * FROM tokens WHERE username=$1 and token=$2;` + getUserTokensQuery = `SELECT * FROM tokens WHERE username=$1;` + deleteTokenQuery = `DELETE FROM tokens WHERE username=$1 AND token=$2;` + updateTokenQuery = `UPDATE tokens SET description=:description, user_agent=:user_agent WHERE token=:token RETURNING *;` +) + +// Custom errors +var ( + ErrTokenNotFound = errors.New("tokens: token not found") +) + +// Token represents a token +type Token struct { + sqly.BaseModel + Username string `db:"username" json:"username"` + Token string `db:"token" json:"token"` + Description string `db:"description" json:"description"` + UserAgent string `db:"user_agent" json:"user_agent"` +} + +// Add a token to the database +func (t *Token) Add(db *sqlx.DB) error { + _, err := db.Queryx(addTokenQuery, t.Token, t.Username) + if err != nil { + return err + } + return nil +} + +// GetUserToken returns the token linked to a user +func GetUserToken(db *sqlx.DB, username, token string) (*Token, error) { + t := &Token{} + err := db.QueryRowx(getUserTokenQuery, username, token).StructScan(t) + if err != nil { + if err == sql.ErrNoRows { + return nil, ErrTokenNotFound + } + return nil, err + } + return t, nil +} + +// DeleteToken deletes a token +func DeleteToken(db *sqlx.DB, username, token string) error { + r, err := db.Exec(deleteTokenQuery, username, token) + if err != nil { + return err + } + + count, err := r.RowsAffected() + if err != nil { + return err + } + + if count != 1 { + return fmt.Errorf("Unexpected number of row deleted: %d", count) + } + + return nil +} + +// GetUserTokens returns all tokens owned by the user +func GetUserTokens(db *sqlx.DB, username string) ([]*Token, error) { + tokens := []*Token{} + err := db.Select(&tokens, getUserTokensQuery, username) + if err != nil { + return nil, err + } + return tokens, nil +} + +// Update updates a token +func (t *Token) Update(db *sqlx.DB) error { + rows, err := db.NamedQuery(updateTokenQuery, t) + if err != nil { + return err + } + for rows.Next() { + rows.StructScan(t) + } + return nil +} diff --git a/backend/users/handlers.go b/backend/users/handlers.go index 2815ca2..f57b4c7 100644 --- a/backend/users/handlers.go +++ b/backend/users/handlers.go @@ -4,12 +4,11 @@ import ( "encoding/json" "fmt" "net/http" - "time" - - jwt "github.com/dgrijalva/jwt-go" + "github.com/gorilla/mux" "gitlab.quimbo.fr/odwrtw/canape/backend/auth" "gitlab.quimbo.fr/odwrtw/canape/backend/config" + "gitlab.quimbo.fr/odwrtw/canape/backend/tokens" "gitlab.quimbo.fr/odwrtw/canape/backend/web" ) @@ -65,37 +64,21 @@ func LoginPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error return err } - user, err := e.Auth.Login(w, r, data.Username, data.Password) + token, err := e.Auth.Login(data.Username, data.Password) if err != nil { if err == auth.ErrInvalidPassword || err == ErrUnknownUser { return e.RenderError(w, fmt.Errorf("Error invalid user or password")) } return err } - e.Log.Debugf("logged %s", user.GetName()) + e.Log.Debugf("logged %s", token.Username) - // 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(), - "isActivated": user.IsActivated(), - }) - - // Sign the token - ss, err := token.SignedString([]byte(e.Auth.Secret)) - if err != nil { - return err - } + // TODO: add token expiration / keep me login stuff var out = struct { Token string `json:"token"` }{ - Token: ss, + Token: token.Token, } return e.RenderJSON(w, out) @@ -167,3 +150,68 @@ func EditHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { return e.RenderOK(w, "user updated") } + +// GetTokensHandler lists the tokens of a user +func GetTokensHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { + v := auth.GetCurrentUser(r, e.Log) + user, ok := v.(*User) + if !ok { + return fmt.Errorf("invalid user type") + } + + tokens, err := tokens.GetUserTokens(e.Database, user.Name) + if err != nil { + return err + } + + return e.RenderJSON(w, tokens) +} + +// DeleteTokenHandler helps delete a token +func DeleteTokenHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + token := vars["token"] + + v := auth.GetCurrentUser(r, e.Log) + user, ok := v.(*User) + if !ok { + return fmt.Errorf("invalid user type") + } + + if err := tokens.DeleteToken(e.Database, user.Name, token); err != nil { + return err + } + + return e.RenderOK(w, "token deleted") +} + +// EditTokenHandler helps delete a token +func EditTokenHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + token := vars["token"] + + v := auth.GetCurrentUser(r, e.Log) + user, ok := v.(*User) + if !ok { + return fmt.Errorf("invalid user type") + } + + t, err := tokens.GetUserToken(e.Database, user.Name, token) + if err != nil { + return err + } + + data := struct { + Description string `json:"description"` + }{} + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + return err + } + + t.Description = data.Description + if err := t.Update(e.Database); err != nil { + return err + } + + return e.RenderJSON(w, t) +} diff --git a/backend/users/users.go b/backend/users/users.go index 00ba547..4c817e8 100644 --- a/backend/users/users.go +++ b/backend/users/users.go @@ -11,7 +11,6 @@ import ( "github.com/odwrtw/papi" "gitlab.quimbo.fr/odwrtw/canape/backend/config" - "gitlab.quimbo.fr/odwrtw/canape/backend/random" "gitlab.quimbo.fr/odwrtw/canape/backend/sqly" ) @@ -22,11 +21,6 @@ const ( updateUserQuery = `UPDATE users SET name=:name, hash=:hash, admin=:admin, activated=:activated, rawconfig=:rawconfig WHERE id=:id RETURNING *;` deleteUseQuery = `DELETE FROM users WHERE id=:id;` - addTokenQuery = `INSERT INTO tokens (value, user_id) VALUES ($1, $2) RETURNING id;` - getTokensQuery = `SELECT id, value FROM tokens WHERE user_id=$1;` - checkTokenQuery = `SELECT count(*) FROM tokens WHERE user_id=$1 AND value=$2;` - deleteTokenQuery = `DELETE FROM tokens WHERE user_id=$1 AND value=$2;` - getAllUsersQuery = `SELECT * FROM users order by created_at;` ) @@ -119,12 +113,6 @@ func (u *User) NewPapiClient() (*papi.Client, error) { return client, nil } -// Token represents a token -type Token struct { - sqly.BaseModel - Value string -} - // Get returns user with specified name func Get(q sqlx.Queryer, name string) (*User, error) { u := &User{} @@ -193,57 +181,6 @@ func (u *User) Delete(ex *sqlx.DB) error { return nil } -// GetTokens returns all tokens owned by the user -func (u *User) GetTokens(ex *sqlx.DB) ([]*Token, error) { - tokens := []*Token{} - err := ex.Select(&tokens, getTokensQuery, u.ID) - if err != nil { - return nil, err - } - return tokens, nil -} - -// NewToken generates a new token for the user -func (u *User) NewToken(ex *sqlx.DB) (*Token, error) { - t := &Token{ - Value: random.String(50), - } - - var id string - err := ex.QueryRowx(addTokenQuery, t.Value, u.ID).Scan(&id) - if err != nil { - return nil, err - } - t.ID = id - return t, nil -} - -// CheckToken checks if specified value exists in token's values for the user -func (u *User) CheckToken(ex *sqlx.DB, value string) (bool, error) { - var count int - err := ex.QueryRowx(checkTokenQuery, u.ID, value).Scan(&count) - if err != nil { - return false, err - } - if count != 1 { - return false, nil - } - return true, nil -} - -// DeleteToken delete token by value -func (u *User) DeleteToken(ex *sqlx.DB, value string) error { - r, err := ex.Exec(deleteTokenQuery, u.ID, value) - if err != nil { - return err - } - count, _ := r.RowsAffected() - if count != 1 { - return fmt.Errorf("Unexpected number of row deleted: %d", count) - } - return nil -} - // GetHash implements auth.User interface func (u *User) GetHash() string { return u.Hash diff --git a/migrations/0006_user_token.down.sql b/migrations/0006_user_token.down.sql new file mode 100644 index 0000000..7da0f43 --- /dev/null +++ b/migrations/0006_user_token.down.sql @@ -0,0 +1 @@ +DROP TABLE tokens; diff --git a/migrations/0006_user_token.up.sql b/migrations/0006_user_token.up.sql new file mode 100644 index 0000000..e26c276 --- /dev/null +++ b/migrations/0006_user_token.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE tokens ( + token text PRIMARY KEY NOT NULL UNIQUE, + username text NOT NULL REFERENCES users(name), + description text NOT NULL DEFAULT '-', + user_agent text NOT NULL DEFAULT '-', + ip inet NOT NULL, + last_used timestamp with time zone NOT NULL DEFAULT current_timestamp, + LIKE base INCLUDING DEFAULTS +); + +CREATE INDEX ON tokens (username); +CREATE TRIGGER update_tokens_updated_at BEFORE UPDATE ON tokens FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); From bd20a87548e7dadced66e19ac197d6a7c9b7d829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Tue, 20 Feb 2018 23:22:32 +0100 Subject: [PATCH 2/4] Add user IP to its token --- backend/auth/auth.go | 4 ++- backend/auth/middleware.go | 60 ++++++++++++++++++++++++++++++++++++-- backend/main.go | 2 ++ backend/tokens/tokens.go | 17 ++++++----- backend/users/handlers.go | 2 +- 5 files changed, 74 insertions(+), 11 deletions(-) diff --git a/backend/auth/auth.go b/backend/auth/auth.go index 6b3c331..08eb044 100644 --- a/backend/auth/auth.go +++ b/backend/auth/auth.go @@ -71,7 +71,7 @@ func (a *Authorizer) GenHash(password string) (string, error) { } // Login cheks password and creates a jwt token -func (a *Authorizer) Login(username, password string) (*tokens.Token, error) { +func (a *Authorizer) Login(r *http.Request, username, password string) (*tokens.Token, error) { u, err := a.Backend.GetUser(username) if err != nil { return nil, err @@ -104,6 +104,7 @@ func (a *Authorizer) Login(username, password string) (*tokens.Token, error) { t := &tokens.Token{ Token: ss, Username: u.GetName(), + IP: getIPFromRequest(r), } if err := t.Add(a.db); err != nil { @@ -168,6 +169,7 @@ func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (Use } token.UserAgent = req.UserAgent() + token.IP = getIPFromRequest(req) if err := token.Update(a.db); err != nil { return nil, err } diff --git a/backend/auth/middleware.go b/backend/auth/middleware.go index c4c9f35..a3e8469 100644 --- a/backend/auth/middleware.go +++ b/backend/auth/middleware.go @@ -2,19 +2,21 @@ package auth import ( "context" + "net" "net/http" "github.com/sirupsen/logrus" ) +type ipContextKey string +type authContextKey string + // Middleware get User from session and put it in context type Middleware struct { authorizer *Authorizer log *logrus.Entry } -type authContextKey string - // NewMiddleware returns a new authentication middleware func NewMiddleware(authorizer *Authorizer, log *logrus.Entry) *Middleware { return &Middleware{ @@ -101,3 +103,57 @@ func GetCurrentUser(r *http.Request, log *logrus.Entry) User { } return user } + +// IPMiddleware set the IP in the request context +type IPMiddleware struct { + log *logrus.Entry +} + +// NewIPMiddleware returns a new ip middleware +func NewIPMiddleware(log *logrus.Entry) *IPMiddleware { + return &IPMiddleware{ + log: log.WithField("middleware", "ip"), + } +} + +func (m *IPMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + ip := getRequestIP(r) + + ctxKey := ipContextKey("ip") + ctx := context.WithValue(r.Context(), ctxKey, ip) + r = r.WithContext(ctx) + + next(w, r) +} + +func getRequestIP(req *http.Request) string { + // Try to get the IP from this header + var ip = req.Header.Get("X-Real-IP") + if ip != "" { + return ip + } + + // Or from this one + ip = req.Header.Get("X-Forwarded-For") + if ip != "" { + return ip + } + + host, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + // fake result + return "0.0.0.0" + } + + hostIP := net.ParseIP(host) + if host == "" { + return "0.0.0.0" + } + + // Default to the IP from the request + return hostIP.String() +} + +func getIPFromRequest(r *http.Request) string { + return r.Context().Value(ipContextKey("ip")).(string) +} diff --git a/backend/main.go b/backend/main.go index 19b0416..da77cf0 100644 --- a/backend/main.go +++ b/backend/main.go @@ -89,6 +89,8 @@ func main() { defer c.Stop() n := negroni.Classic() + // Middleware for setting ips + n.Use(auth.NewIPMiddleware(log)) // Middleware for authentication n.Use(authMiddleware) // Serve static files diff --git a/backend/tokens/tokens.go b/backend/tokens/tokens.go index d25f24a..e76c366 100644 --- a/backend/tokens/tokens.go +++ b/backend/tokens/tokens.go @@ -4,18 +4,19 @@ import ( "database/sql" "errors" "fmt" + "time" "github.com/jmoiron/sqlx" "gitlab.quimbo.fr/odwrtw/canape/backend/sqly" ) const ( - addTokenQuery = `INSERT INTO tokens (token, username) VALUES ($1, $2);` + addTokenQuery = `INSERT INTO tokens (token, username, ip) VALUES ($1, $2, $3);` getTokenQuery = `SELECT * FROM tokens WHERE token=$1;` getUserTokenQuery = `SELECT * FROM tokens WHERE username=$1 and token=$2;` getUserTokensQuery = `SELECT * FROM tokens WHERE username=$1;` deleteTokenQuery = `DELETE FROM tokens WHERE username=$1 AND token=$2;` - updateTokenQuery = `UPDATE tokens SET description=:description, user_agent=:user_agent WHERE token=:token RETURNING *;` + updateTokenQuery = `UPDATE tokens SET description=:description, user_agent=:user_agent, ip=:ip, last_used=now() WHERE token=:token RETURNING *;` ) // Custom errors @@ -26,15 +27,17 @@ var ( // Token represents a token type Token struct { sqly.BaseModel - Username string `db:"username" json:"username"` - Token string `db:"token" json:"token"` - Description string `db:"description" json:"description"` - UserAgent string `db:"user_agent" json:"user_agent"` + Username string `db:"username" json:"username"` + Token string `db:"token" json:"token"` + Description string `db:"description" json:"description"` + UserAgent string `db:"user_agent" json:"user_agent"` + IP string `db:"ip" json:"ip"` + LastUsed time.Time `db:"last_used" json:"last_used"` } // Add a token to the database func (t *Token) Add(db *sqlx.DB) error { - _, err := db.Queryx(addTokenQuery, t.Token, t.Username) + _, err := db.Queryx(addTokenQuery, t.Token, t.Username, t.IP) if err != nil { return err } diff --git a/backend/users/handlers.go b/backend/users/handlers.go index f57b4c7..f75f382 100644 --- a/backend/users/handlers.go +++ b/backend/users/handlers.go @@ -64,7 +64,7 @@ func LoginPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error return err } - token, err := e.Auth.Login(data.Username, data.Password) + token, err := e.Auth.Login(r, data.Username, data.Password) if err != nil { if err == auth.ErrInvalidPassword || err == ErrUnknownUser { return e.RenderError(w, fmt.Errorf("Error invalid user or password")) From 2ddca462daa301225c86b2f5ffd0b4a11eaa61c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Tue, 13 Mar 2018 23:22:10 +0100 Subject: [PATCH 3/4] Add a page to list / delete the user tokens --- frontend/js/actions/users.js | 17 +++ frontend/js/components/navbar.js | 5 + frontend/js/components/users/tokens.js | 187 +++++++++++++++++++++++++ frontend/js/reducers/users.js | 6 +- frontend/js/routes.js | 12 +- frontend/less/app.less | 9 ++ package.json | 2 + yarn.lock | 6 +- 8 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 frontend/js/components/users/tokens.js diff --git a/frontend/js/actions/users.js b/frontend/js/actions/users.js index 686a951..d8d6844 100644 --- a/frontend/js/actions/users.js +++ b/frontend/js/actions/users.js @@ -50,3 +50,20 @@ export function getUserInfos() { configureAxios().get("/users/details") ) } + +export function getUserTokens() { + return request( + "GET_USER_TOKENS", + configureAxios().get("/users/tokens") + ) +} + +export function deleteUserToken(token) { + return request( + "DELETE_USER_TOKEN", + configureAxios().delete(`/users/tokens/${token}`), + [ + () => getUserTokens(), + ] + ) +} diff --git a/frontend/js/components/navbar.js b/frontend/js/components/navbar.js index 19ae1f9..008a82b 100644 --- a/frontend/js/components/navbar.js +++ b/frontend/js/components/navbar.js @@ -164,6 +164,11 @@ function UserDropdown(props) { Edit } + {props.isActivated && + + Tokens + + } Logout diff --git a/frontend/js/components/users/tokens.js b/frontend/js/components/users/tokens.js new file mode 100644 index 0000000..adb620b --- /dev/null +++ b/frontend/js/components/users/tokens.js @@ -0,0 +1,187 @@ +import React from "react" +import { connect } from "react-redux" +import { UAParser } from "ua-parser-js" +import moment from "moment" + +import { bindActionCreators } from "redux" +import { deleteUserToken } from "../../actions/users" + +const mapDispatchToProps = (dispatch) => + bindActionCreators({ deleteUserToken }, dispatch) + +function mapStateToProps(state) { + return { + tokens: state.userStore.get("tokens"), + }; +} + +function UserTokens(props) { + return ( +
+
+ +
+
+ ); +} + +function TokenList(props) { + return ( +
+

Tokens

+

Tokens

+ +
+ {props.tokens.map(function(el, index) { + return ( + + ); + })} +
+
+ ); +} + +function Token(props) { + const ua = UAParser(props.data.get("user_agent")); + return ( +
+
+ +
+
+
Description
+
{props.data.get("description")}
+ +
Last IP
+
{props.data.get("ip")}
+ +
Last used
+
{moment(props.data.get("last_used")).fromNow()}
+ +
Created
+
{moment(props.data.get("created_at")).fromNow()}
+ +
Device
+
+ +
OS
+
+ +
Browser
+
+
+
+ +
+
+ ); +} + +class Actions extends React.PureComponent { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + handleClick() { + const token = this.props.data.get("token"); + this.props.deleteToken(token); + } + render() { + return ( +
+ + +
+ ); + } +} + +function Logo(props) { + var className; + if (props.ua === "canape-cli") { + className = "terminal"; + } else if (props.device.type == "mobile" ){ + className = "mobile"; + } else { + switch (props.browser.name) { + case "Chrome": + case "chrome": + className = "chrome"; + break; + case "Safari": + case "safari": + className = "safari"; + break; + case "Firefox": + case "firefox": + className = "firefox"; + break; + default: + className = "question"; + break; + } + } + + return ( +
+
+ +
+
+ +
+
+ ); +} + +function OS(props) { + var osName = "-"; + + if (props.name !== undefined) { + osName = props.name; + + if (props.version !== undefined) { + osName += " " + props.version; + } + } + + return ( + {osName} + ); +} + +function Device(props) { + var deviceName = "-"; + + if (props.model !== undefined) { + deviceName = props.model; + } + + return ( + {deviceName} + ); +} + +function Browser(props) { + var browserName = "-"; + if (props.name !== undefined) { + browserName = props.name; + + if (props.version !== undefined) { + browserName += " - " + props.version; + } + } + + return ( + {browserName} + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(UserTokens); diff --git a/frontend/js/reducers/users.js b/frontend/js/reducers/users.js index 8e133e7..116f4d6 100644 --- a/frontend/js/reducers/users.js +++ b/frontend/js/reducers/users.js @@ -1,4 +1,4 @@ -import { Map } from "immutable" +import { Map, List, fromJS } from "immutable" import jwtDecode from "jwt-decode" import Cookies from "universal-cookie" @@ -11,6 +11,7 @@ const defaultState = Map({ isLogged: false, polochonToken: "", polochonUrl: "", + tokens: List(), }); const handlers = { @@ -27,6 +28,9 @@ const handlers = { polochonToken: action.payload.response.data.token, polochonUrl: action.payload.response.data.url, })), + "GET_USER_TOKENS_FULFILLED": (state, action) => state.set( + "tokens", fromJS(action.payload.response.data) + ), } function logoutUser() { diff --git a/frontend/js/routes.js b/frontend/js/routes.js index 20a0b91..86596db 100644 --- a/frontend/js/routes.js +++ b/frontend/js/routes.js @@ -3,6 +3,7 @@ import ShowList from "./components/shows/list" import ShowDetails from "./components/shows/details" import UserLoginForm from "./components/users/login" import UserEdit from "./components/users/edit" +import UserTokens from "./components/users/tokens" import UserActivation from "./components/users/activation" import UserSignUp from "./components/users/signup" import TorrentList from "./components/torrents/list" @@ -10,7 +11,7 @@ import TorrentSearch from "./components/torrents/search" import AdminPanel from "./components/admins/panel" import { fetchTorrents, searchTorrents } from "./actions/torrents" -import { userLogout, getUserInfos } from "./actions/users" +import { userLogout, getUserInfos, getUserTokens } from "./actions/users" import { fetchMovies, getMovieExploreOptions } from "./actions/movies" import { fetchShows, fetchShowDetails, getShowExploreOptions } from "./actions/shows" import { getUsers, getStats } from "./actions/admins" @@ -118,6 +119,15 @@ export default function getRoutes(App) { }); }, }, + { + path: "/users/tokens", + component: UserTokens, + onEnter: function(nextState, replace, next) { + loginCheck(nextState, replace, next, function() { + store.dispatch(getUserTokens()); + }); + }, + }, { path: "/users/activation", component: UserActivation, diff --git a/frontend/less/app.less b/frontend/less/app.less index a59f0ae..4b51f59 100644 --- a/frontend/less/app.less +++ b/frontend/less/app.less @@ -53,6 +53,7 @@ body { max-height: 300px; } +.spaced-icons, .episode-buttons { div, span { margin: 2px; @@ -103,3 +104,11 @@ table.torrent-search-result { table.table-align-middle > tbody > tr > td { vertical-align: middle; } + +div.user-token-icon > div { + padding-top: 1%; +} + +span.user-token-action { + margin: 10px; +} diff --git a/package.json b/package.json index b77a583..e00a280 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "immutable": "^3.8.1", "jquery": "^2.2.4", "jwt-decode": "^2.1.0", + "moment": "^2.20.1", "react": "^16.2.0", "react-bootstrap": "^0.32.1", "react-bootstrap-sweetalert": "^4.2.3", @@ -31,6 +32,7 @@ "redux": "^3.7.2", "redux-logger": "^3.0.6", "redux-thunk": "^2.2.0", + "ua-parser-js": "^0.7.17", "universal-cookie": "^2.1.2", "webpack": "^3.11.0" }, diff --git a/yarn.lock b/yarn.lock index 08429df..fdefbf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2975,6 +2975,10 @@ minimist@^1.2.0: dependencies: minimist "0.0.8" +moment@^2.20.1: + version "2.20.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -4399,7 +4403,7 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -ua-parser-js@^0.7.9: +ua-parser-js@^0.7.17, ua-parser-js@^0.7.9: version "0.7.17" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" From 9db07de89ee1d797f7a337f6b726a8772effa0e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Wed, 14 Mar 2018 13:20:33 +0100 Subject: [PATCH 4/4] Remove the random package --- backend/random/random.go | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 backend/random/random.go diff --git a/backend/random/random.go b/backend/random/random.go deleted file mode 100644 index fa9f057..0000000 --- a/backend/random/random.go +++ /dev/null @@ -1,38 +0,0 @@ -package random - -import ( - "math/rand" - "time" -) - -func init() { - rand.Seed(time.Now().UnixNano()) -} - -const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" -const ( - letterIdxBits = 6 // 6 bits to represent a letter index - letterIdxMask = 1<= 0; { - if remain == 0 { - cache, remain = src.Int63(), letterIdxMax - } - if idx := int(cache & letterIdxMask); idx < len(letterBytes) { - b[i] = letterBytes[idx] - i-- - } - cache >>= letterIdxBits - remain-- - } - - return string(b) -}