Authenticate UI
This commit is contained in:
@@ -8,11 +8,13 @@ require (
|
|||||||
github.com/astaxie/beego v1.12.0
|
github.com/astaxie/beego v1.12.0
|
||||||
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible
|
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible
|
||||||
github.com/deluan/rest v0.0.0-20200114062534-0653ffe9eab4
|
github.com/deluan/rest v0.0.0-20200114062534-0653ffe9eab4
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
github.com/dhowden/tag v0.0.0-20191122115059-7e5c04feccd8
|
github.com/dhowden/tag v0.0.0-20191122115059-7e5c04feccd8
|
||||||
github.com/fatih/camelcase v0.0.0-20160318181535-f6a740d52f96 // indirect
|
github.com/fatih/camelcase v0.0.0-20160318181535-f6a740d52f96 // indirect
|
||||||
github.com/fatih/structs v1.0.0 // indirect
|
github.com/fatih/structs v1.0.0 // indirect
|
||||||
github.com/go-chi/chi v4.0.3+incompatible
|
github.com/go-chi/chi v4.0.3+incompatible
|
||||||
github.com/go-chi/cors v1.0.0
|
github.com/go-chi/cors v1.0.0
|
||||||
|
github.com/go-chi/jwtauth v4.0.3+incompatible
|
||||||
github.com/google/uuid v1.1.1
|
github.com/google/uuid v1.1.1
|
||||||
github.com/google/wire v0.4.0
|
github.com/google/wire v0.4.0
|
||||||
github.com/kennygrant/sanitize v0.0.0-20170120101633-6a0bfdde8629
|
github.com/kennygrant/sanitize v0.0.0-20170120101633-6a0bfdde8629
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/deluan/rest v0.0.0-20200114062534-0653ffe9eab4 h1:VSoAcWJvj656TSyWbJ5KuGsi/J8dO5+iO9+5/7I8wao=
|
github.com/deluan/rest v0.0.0-20200114062534-0653ffe9eab4 h1:VSoAcWJvj656TSyWbJ5KuGsi/J8dO5+iO9+5/7I8wao=
|
||||||
github.com/deluan/rest v0.0.0-20200114062534-0653ffe9eab4/go.mod h1:tSgDythFsl0QgS/PFWfIZqcJKnkADWneY80jaVRlqK8=
|
github.com/deluan/rest v0.0.0-20200114062534-0653ffe9eab4/go.mod h1:tSgDythFsl0QgS/PFWfIZqcJKnkADWneY80jaVRlqK8=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dhowden/tag v0.0.0-20191122115059-7e5c04feccd8 h1:nmFnmD8VZXkjDHIb1Gnfz50cgzUvGN72zLjPRXBW/hU=
|
github.com/dhowden/tag v0.0.0-20191122115059-7e5c04feccd8 h1:nmFnmD8VZXkjDHIb1Gnfz50cgzUvGN72zLjPRXBW/hU=
|
||||||
github.com/dhowden/tag v0.0.0-20191122115059-7e5c04feccd8/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw=
|
github.com/dhowden/tag v0.0.0-20191122115059-7e5c04feccd8/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw=
|
||||||
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8=
|
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8=
|
||||||
@@ -39,6 +41,8 @@ github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8q
|
|||||||
github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||||
github.com/go-chi/cors v1.0.0 h1:e6x8k7uWbUwYs+aXDoiUzeQFT6l0cygBYyNhD7/1Tg0=
|
github.com/go-chi/cors v1.0.0 h1:e6x8k7uWbUwYs+aXDoiUzeQFT6l0cygBYyNhD7/1Tg0=
|
||||||
github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
|
github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
|
||||||
|
github.com/go-chi/jwtauth v4.0.3+incompatible h1:hPhobLUgh7fMpA1qUDdId14u2Z93M22fCNPMVLNWeHU=
|
||||||
|
github.com/go-chi/jwtauth v4.0.3+incompatible/go.mod h1:Q5EIArY/QnD6BdS+IyDw7B2m6iNbnPxtfd6/BcmtWbs=
|
||||||
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import "time"
|
|||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string
|
ID string
|
||||||
|
UserName string
|
||||||
Name string
|
Name string
|
||||||
|
Email string
|
||||||
Password string
|
Password string
|
||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
LastLoginAt *time.Time
|
LastLoginAt *time.Time
|
||||||
@@ -17,4 +19,6 @@ type UserRepository interface {
|
|||||||
CountAll(...QueryOptions) (int64, error)
|
CountAll(...QueryOptions) (int64, error)
|
||||||
Get(id string) (*User, error)
|
Get(id string) (*User, error)
|
||||||
Put(*User) error
|
Put(*User) error
|
||||||
|
FindByUsername(username string) (*User, error)
|
||||||
|
UpdateLastLoginAt(id string) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import (
|
|||||||
|
|
||||||
type user struct {
|
type user struct {
|
||||||
ID string `json:"id" orm:"pk;column(id)"`
|
ID string `json:"id" orm:"pk;column(id)"`
|
||||||
Name string `json:"name" orm:"index"`
|
UserName string `json:"userName" orm:"index;unique"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email" orm:"unique"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
LastLoginAt *time.Time `json:"lastLoginAt" orm:"null"`
|
LastLoginAt *time.Time `json:"lastLoginAt" orm:"null"`
|
||||||
@@ -24,6 +26,12 @@ type userRepository struct {
|
|||||||
userResource model.ResourceRepository
|
userResource model.ResourceRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewUserRepository(o orm.Ormer) model.UserRepository {
|
||||||
|
r := &userRepository{ormer: o}
|
||||||
|
r.userResource = NewResource(o, model.User{}, new(user))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func (r *userRepository) CountAll(qo ...model.QueryOptions) (int64, error) {
|
func (r *userRepository) CountAll(qo ...model.QueryOptions) (int64, error) {
|
||||||
if len(qo) > 0 {
|
if len(qo) > 0 {
|
||||||
return r.userResource.Count(rest.QueryOptions(qo[0]))
|
return r.userResource.Count(rest.QueryOptions(qo[0]))
|
||||||
@@ -41,22 +49,36 @@ func (r *userRepository) Get(id string) (*model.User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *userRepository) Put(u *model.User) error {
|
func (r *userRepository) Put(u *model.User) error {
|
||||||
|
tu := user(*u)
|
||||||
c, err := r.CountAll()
|
c, err := r.CountAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if c == 0 {
|
if c == 0 {
|
||||||
mappedUser := user(*u)
|
_, err = r.userResource.Save(&tu)
|
||||||
_, err = r.userResource.Save(&mappedUser)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return r.userResource.Update(u, "name", "is_admin", "password")
|
return r.userResource.Update(&tu, "user_name", "is_admin", "password")
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserRepository(o orm.Ormer) model.UserRepository {
|
func (r *userRepository) FindByUsername(username string) (*model.User, error) {
|
||||||
r := &userRepository{ormer: o}
|
tu := user{}
|
||||||
r.userResource = NewResource(o, model.User{}, new(user))
|
err := r.ormer.QueryTable(user{}).Filter("user_name", username).One(&tu)
|
||||||
return r
|
if err == orm.ErrNoRows {
|
||||||
|
return nil, model.ErrNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
u := model.User(tu)
|
||||||
|
return &u, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) UpdateLastLoginAt(id string) error {
|
||||||
|
now := time.Now()
|
||||||
|
tu := user{ID: id, LastLoginAt: &now}
|
||||||
|
_, err := r.ormer.Update(&tu, "last_login_at")
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = model.User(user{})
|
var _ = model.User(user{})
|
||||||
|
|||||||
+14
-8
@@ -12,10 +12,15 @@ import (
|
|||||||
"github.com/cloudsonic/sonic-server/server"
|
"github.com/cloudsonic/sonic-server/server"
|
||||||
"github.com/deluan/rest"
|
"github.com/deluan/rest"
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
|
"github.com/go-chi/jwtauth"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const initialUser = "admin"
|
var initialUser = model.User{
|
||||||
|
UserName: "admin",
|
||||||
|
Name: "Admin",
|
||||||
|
IsAdmin: true,
|
||||||
|
}
|
||||||
|
|
||||||
type Router struct {
|
type Router struct {
|
||||||
ds model.DataStore
|
ds model.DataStore
|
||||||
@@ -43,8 +48,12 @@ func (app *Router) routes() http.Handler {
|
|||||||
// Basic unauthenticated ping
|
// Basic unauthenticated ping
|
||||||
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"response":"pong"}`)) })
|
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"response":"pong"}`)) })
|
||||||
|
|
||||||
|
r.Post("/login", Login(app.ds))
|
||||||
|
|
||||||
r.Route("/api", func(r chi.Router) {
|
r.Route("/api", func(r chi.Router) {
|
||||||
// Add User resource
|
// Add User resource
|
||||||
|
r.Use(jwtauth.Verifier(TokenAuth))
|
||||||
|
r.Use(Authenticator)
|
||||||
R(r, "/user", func(ctx context.Context) rest.Repository {
|
R(r, "/user", func(ctx context.Context) rest.Repository {
|
||||||
return app.ds.Resource(model.User{})
|
return app.ds.Resource(model.User{})
|
||||||
})
|
})
|
||||||
@@ -60,13 +69,10 @@ func (app *Router) createDefaultUser() {
|
|||||||
if c == 0 {
|
if c == 0 {
|
||||||
id, _ := uuid.NewRandom()
|
id, _ := uuid.NewRandom()
|
||||||
initialPassword, _ := uuid.NewRandom()
|
initialPassword, _ := uuid.NewRandom()
|
||||||
log.Warn("Creating initial user. Please change the password!", "user", initialUser, "password", initialPassword)
|
log.Warn("Creating initial user. Please change the password!", "user", initialUser.UserName, "password", initialPassword)
|
||||||
app.ds.User().Put(&model.User{
|
initialUser.ID = id.String()
|
||||||
ID: id.String(),
|
initialUser.Password = initialPassword.String()
|
||||||
Name: initialUser,
|
app.ds.User().Put(&initialUser)
|
||||||
Password: initialPassword.String(),
|
|
||||||
IsAdmin: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cloudsonic/sonic-server/model"
|
||||||
|
"github.com/deluan/rest"
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/go-chi/jwtauth"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tokenExpiration = 30 * time.Minute
|
||||||
|
issuer = "CloudSonic"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
jwtSecret []byte
|
||||||
|
TokenAuth *jwtauth.JWTAuth
|
||||||
|
)
|
||||||
|
|
||||||
|
func Login(ds model.DataStore) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := make(map[string]string)
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
if err := decoder.Decode(&data); err != nil {
|
||||||
|
log.Errorf("parsing request body: %#v", err)
|
||||||
|
rest.RespondWithError(w, http.StatusUnprocessableEntity, "Invalid request payload")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := data["username"]
|
||||||
|
password := data["password"]
|
||||||
|
|
||||||
|
user, err := validateLogin(ds.User(), username, password)
|
||||||
|
if err != nil {
|
||||||
|
rest.RespondWithError(w, http.StatusInternalServerError, "Unknown error authentication user. Please try again")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
log.Warnf("Unsuccessful login: '%s', request: %v", username, r.Header)
|
||||||
|
rest.RespondWithError(w, http.StatusUnauthorized, "Invalid username or password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenString, err := createToken(user)
|
||||||
|
if err != nil {
|
||||||
|
rest.RespondWithError(w, http.StatusInternalServerError, "Unknown error authenticating user. Please try again")
|
||||||
|
}
|
||||||
|
rest.RespondWithJSON(w, http.StatusOK,
|
||||||
|
map[string]interface{}{
|
||||||
|
"message": "User '" + username + "' authenticated successfully",
|
||||||
|
"token": tokenString,
|
||||||
|
"user": strings.Title(user.UserName),
|
||||||
|
"username": username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func validateLogin(userRepo model.UserRepository, userName, password string) (*model.User, error) {
|
||||||
|
u, err := userRepo.FindByUsername(userName)
|
||||||
|
if err == model.ErrNotFound {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if u.Password != password {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
err = userRepo.UpdateLastLoginAt(u.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Could not update LastLoginAt", "user", userName)
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createToken(u *model.User) (string, error) {
|
||||||
|
token := jwt.New(jwt.SigningMethodHS256)
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
claims["iss"] = issuer
|
||||||
|
claims["sub"] = u.UserName
|
||||||
|
|
||||||
|
return touchToken(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func touchToken(token *jwt.Token) (string, error) {
|
||||||
|
expireIn := time.Now().Add(tokenExpiration).Unix()
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
claims["exp"] = expireIn
|
||||||
|
|
||||||
|
return token.SignedString(jwtSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userFrom(claims jwt.MapClaims) *model.User {
|
||||||
|
user := &model.User{
|
||||||
|
UserName: claims["sub"].(string),
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func Authenticator(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token, _, err := jwtauth.FromContext(r.Context())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.RespondWithError(w, http.StatusUnauthorized, "Not authenticated")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if token == nil || !token.Valid {
|
||||||
|
rest.RespondWithError(w, http.StatusUnauthorized, "Not authenticated")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
|
||||||
|
newCtx := context.WithValue(r.Context(), "loggedUser", userFrom(claims))
|
||||||
|
newTokenString, err := touchToken(token)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("signing new token: %v", err)
|
||||||
|
rest.RespondWithError(w, http.StatusUnauthorized, "Not authenticated")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Authorization", newTokenString)
|
||||||
|
next.ServeHTTP(w, r.WithContext(newCtx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// TODO Store jwtSecret in the DB
|
||||||
|
secret := os.Getenv("JWT_SECRET")
|
||||||
|
if secret == "" {
|
||||||
|
secret = "not so secret"
|
||||||
|
log.Warn("No JWT_SECRET env var found. Please set one.")
|
||||||
|
}
|
||||||
|
jwtSecret = []byte(secret)
|
||||||
|
TokenAuth = jwtauth.New("HS256", jwtSecret, nil)
|
||||||
|
}
|
||||||
Generated
+5
@@ -8834,6 +8834,11 @@
|
|||||||
"object.assign": "^4.1.0"
|
"object.assign": "^4.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jwt-decode": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
|
||||||
|
"integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk="
|
||||||
|
},
|
||||||
"killable": {
|
"killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"@testing-library/jest-dom": "^5.0.0",
|
"@testing-library/jest-dom": "^5.0.0",
|
||||||
"@testing-library/react": "^9.3.2",
|
"@testing-library/react": "^9.3.2",
|
||||||
"@testing-library/user-event": "^7.1.2",
|
"@testing-library/user-event": "^7.1.2",
|
||||||
|
"jwt-decode": "^2.2.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"ra-data-json-server": "^3.1.2",
|
"ra-data-json-server": "^3.1.2",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
|
|||||||
+7
-3
@@ -1,13 +1,17 @@
|
|||||||
// in src/App.js
|
// in src/App.js
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Admin, Resource } from 'react-admin'
|
import { Admin, Resource } from 'react-admin'
|
||||||
import jsonServerProvider from 'ra-data-json-server'
|
import dataProvider from './dataProvider'
|
||||||
|
import authProvider from './authProvider'
|
||||||
import { Login } from './layout'
|
import { Login } from './layout'
|
||||||
import user from './user'
|
import user from './user'
|
||||||
|
|
||||||
const dataProvider = jsonServerProvider('/app/api')
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<Admin dataProvider={dataProvider} loginPage={Login}>
|
<Admin
|
||||||
|
dataProvider={dataProvider}
|
||||||
|
authProvider={authProvider}
|
||||||
|
loginPage={Login}
|
||||||
|
>
|
||||||
<Resource name="user" {...user} />
|
<Resource name="user" {...user} />
|
||||||
</Admin>
|
</Admin>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import jwtDecode from 'jwt-decode'
|
||||||
|
|
||||||
|
const authProvider = {
|
||||||
|
login: ({ username, password }) => {
|
||||||
|
const request = new Request('/app/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
headers: new Headers({ 'Content-Type': 'application/json' })
|
||||||
|
})
|
||||||
|
return fetch(request)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status < 200 || response.status >= 300) {
|
||||||
|
throw new Error(response.statusText)
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
// Validate token
|
||||||
|
jwtDecode(response.token)
|
||||||
|
localStorage.setItem('token', response.token)
|
||||||
|
localStorage.setItem('name', response.name)
|
||||||
|
localStorage.setItem('username', response.username)
|
||||||
|
return response
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (
|
||||||
|
error.message === 'Failed to fetch' ||
|
||||||
|
error.stack === 'TypeError: Failed to fetch'
|
||||||
|
) {
|
||||||
|
throw new Error('errors.network_error')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: () => {
|
||||||
|
removeItems()
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
|
||||||
|
checkAuth: () => {
|
||||||
|
try {
|
||||||
|
const expireTime = jwtDecode(localStorage.getItem('token')).exp * 1000
|
||||||
|
const now = new Date().getTime()
|
||||||
|
return now < expireTime ? Promise.resolve() : Promise.reject()
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
checkError: (error) => {
|
||||||
|
const { status } = error
|
||||||
|
// TODO Remove 403?
|
||||||
|
if (status === 401 || status === 403) {
|
||||||
|
removeItems()
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
|
||||||
|
getPermissions: (params) => Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeItems = () => {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('name')
|
||||||
|
localStorage.removeItem('username')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default authProvider
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { fetchUtils } from 'react-admin'
|
||||||
|
import jsonServerProvider from 'ra-data-json-server'
|
||||||
|
|
||||||
|
const httpClient = (url, options = {}) => {
|
||||||
|
if (!options.headers) {
|
||||||
|
options.headers = new Headers({ Accept: 'application/json' })
|
||||||
|
}
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token) {
|
||||||
|
options.headers.set('Authorization', `Bearer ${token}`)
|
||||||
|
}
|
||||||
|
return fetchUtils.fetchJson(url, options).then((response) => {
|
||||||
|
const token = response.headers.get('authorization')
|
||||||
|
if (token) {
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataProvider = jsonServerProvider('/app/api', httpClient)
|
||||||
|
|
||||||
|
export default dataProvider
|
||||||
@@ -5,13 +5,16 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
required,
|
required,
|
||||||
|
email,
|
||||||
SimpleForm
|
SimpleForm
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
|
|
||||||
const UserCreate = (props) => (
|
const UserCreate = (props) => (
|
||||||
<Create {...props}>
|
<Create {...props}>
|
||||||
<SimpleForm redirect="list">
|
<SimpleForm redirect="list">
|
||||||
|
<TextInput source="userName" validate={[required()]} />
|
||||||
<TextInput source="name" validate={[required()]} />
|
<TextInput source="name" validate={[required()]} />
|
||||||
|
<TextInput source="email" validate={[required(), email()]} />
|
||||||
<PasswordInput source="password" validate={[required()]} />
|
<PasswordInput source="password" validate={[required()]} />
|
||||||
<BooleanInput source="isAdmin" initialValue={false} />
|
<BooleanInput source="isAdmin" initialValue={false} />
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
PasswordInput,
|
PasswordInput,
|
||||||
Edit,
|
Edit,
|
||||||
required,
|
required,
|
||||||
|
email,
|
||||||
SimpleForm
|
SimpleForm
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
|
|
||||||
@@ -15,7 +16,9 @@ const UserTitle = ({ record }) => {
|
|||||||
const UserEdit = (props) => (
|
const UserEdit = (props) => (
|
||||||
<Edit title={<UserTitle />} {...props}>
|
<Edit title={<UserTitle />} {...props}>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
|
<TextInput source="userName" validate={[required()]} />
|
||||||
<TextInput source="name" validate={[required()]} />
|
<TextInput source="name" validate={[required()]} />
|
||||||
|
<TextInput source="email" validate={[required(), email()]} />
|
||||||
<PasswordInput source="password" validate={[required()]} />
|
<PasswordInput source="password" validate={[required()]} />
|
||||||
<BooleanInput source="isAdmin" initialValue={false} />
|
<BooleanInput source="isAdmin" initialValue={false} />
|
||||||
<DateField source="lastLoginAt" />
|
<DateField source="lastLoginAt" />
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const UserList = (props) => {
|
|||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
sort={{ field: 'name', order: 'ASC' }}
|
sort={{ field: 'userName', order: 'ASC' }}
|
||||||
exporter={false}
|
exporter={false}
|
||||||
filters={<UserFilter />}
|
filters={<UserFilter />}
|
||||||
>
|
>
|
||||||
@@ -34,9 +34,8 @@ const UserList = (props) => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Datagrid rowClick="edit">
|
<Datagrid rowClick="edit">
|
||||||
<TextField source="name" />
|
<TextField source="userName" />
|
||||||
<BooleanField source="isAdmin" />
|
<BooleanField source="isAdmin" />
|
||||||
<DateField source="lastLoginAt" locales="pt-BR" />
|
|
||||||
<DateField source="lastAccessAt" locales="pt-BR" />
|
<DateField source="lastAccessAt" locales="pt-BR" />
|
||||||
<DateField source="updatedAt" locales="pt-BR" />
|
<DateField source="updatedAt" locales="pt-BR" />
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
|
|||||||
Reference in New Issue
Block a user