Web client refinement (#16)
* fix(files/handler): add base64 decode for content * fix(singleuser): pick user name from jwt token and encode content * fix(singleuser): add public path check, abstract user info from token * fix(singleuser): update singleuser client * fix(server): fix test and enable auth by default * feat(client/web): add web client * fix(client/web): refine css styles * fix(client/web): refine styles * fix(client/web): refine styles, add test and fix bugs * test(client/web): add web client tests * fix(client/web): refactor client interface and enhance the robustness * chore(client/web): ignore js bundles * test(files): call sync before check Co-authored-by: Jia He <jiah@nvidia.com>
This commit is contained in:
parent
0265baf1b1
commit
ba6a5373d1
53 changed files with 9192 additions and 158 deletions
|
@ -2,6 +2,7 @@ package fileshdr
|
|||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -32,6 +33,8 @@ var (
|
|||
rangeHeader = "Range"
|
||||
acceptRangeHeader = "Accept-Range"
|
||||
ifRangeHeader = "If-Range"
|
||||
keepAliveHeader = "Keep-Alive"
|
||||
connectionHeader = "Connection"
|
||||
)
|
||||
|
||||
type FileHandlers struct {
|
||||
|
@ -73,17 +76,22 @@ func (h *FileHandlers) NewAutoLocker(c *gin.Context, key string) *AutoLocker {
|
|||
func (lk *AutoLocker) Exec(handler func()) {
|
||||
var err error
|
||||
kv := lk.h.deps.KV()
|
||||
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
fmt.Println(p)
|
||||
}
|
||||
if err = kv.Unlock(lk.key); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = kv.TryLock(lk.key); err != nil {
|
||||
lk.c.JSON(q.Resp(500))
|
||||
lk.c.JSON(q.ErrResp(lk.c, 500, errors.New("fail to lock the file")))
|
||||
return
|
||||
}
|
||||
|
||||
handler()
|
||||
|
||||
if err = kv.Unlock(lk.key); err != nil {
|
||||
// TODO: use logger
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
type CreateReq struct {
|
||||
|
@ -118,9 +126,9 @@ func (h *FileHandlers) Create(c *gin.Context) {
|
|||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
c.JSON(q.Resp(200))
|
||||
c.JSON(q.Resp(200))
|
||||
})
|
||||
}
|
||||
|
||||
func (h *FileHandlers) Delete(c *gin.Context) {
|
||||
|
@ -255,12 +263,21 @@ func (h *FileHandlers) UploadChunk(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
wrote, err := h.deps.FS().WriteAt(tmpFilePath, []byte(req.Content), req.Offset)
|
||||
content, err := base64.StdEncoding.DecodeString(req.Content)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
err = h.uploadMgr.IncreUploaded(tmpFilePath, int64(wrote))
|
||||
|
||||
fmt.Println("length", len([]byte(content)))
|
||||
|
||||
wrote, err := h.deps.FS().WriteAt(tmpFilePath, []byte(content), req.Offset)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
|
||||
err = h.uploadMgr.SetUploaded(tmpFilePath, req.Offset+int64(wrote))
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
|
@ -308,7 +325,11 @@ func (h *FileHandlers) UploadStatus(c *gin.Context) {
|
|||
locker.Exec(func() {
|
||||
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(tmpFilePath)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
if os.IsNotExist(err) {
|
||||
c.JSON(q.ErrResp(c, 404, err))
|
||||
} else {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -337,7 +358,7 @@ func (h *FileHandlers) Download(c *gin.Context) {
|
|||
info, err := h.deps.FS().Stat(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.JSON(q.ErrResp(c, 400, os.ErrNotExist))
|
||||
c.JSON(q.ErrResp(c, 404, os.ErrNotExist))
|
||||
} else {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
}
|
||||
|
|
|
@ -37,17 +37,13 @@ func (um *UploadMgr) AddInfo(fileName, tmpName string, fileSize int64, isDir boo
|
|||
return um.kv.SetString(infoKey(tmpName, filePathKey), fileName)
|
||||
}
|
||||
|
||||
func (um *UploadMgr) IncreUploaded(fileName string, newUploaded int64) error {
|
||||
func (um *UploadMgr) SetUploaded(fileName string, newUploaded int64) error {
|
||||
fileSize, ok := um.kv.GetInt64(infoKey(fileName, fileSizeKey))
|
||||
if !ok {
|
||||
return fmt.Errorf("file size %s not found", fileName)
|
||||
}
|
||||
preUploaded, ok := um.kv.GetInt64(infoKey(fileName, uploadedKey))
|
||||
if !ok {
|
||||
return fmt.Errorf("file uploaded %s not found", fileName)
|
||||
}
|
||||
if newUploaded+preUploaded <= fileSize {
|
||||
um.kv.SetInt64(infoKey(fileName, uploadedKey), newUploaded+preUploaded)
|
||||
if newUploaded <= fileSize {
|
||||
um.kv.SetInt64(infoKey(fileName, uploadedKey), newUploaded)
|
||||
return nil
|
||||
}
|
||||
return errors.New("uploaded is greater than file size")
|
||||
|
|
|
@ -89,6 +89,15 @@ type LoginReq struct {
|
|||
Pwd string `json:"pwd"`
|
||||
}
|
||||
|
||||
func (h *SimpleUserHandlers) checkPwd(user, pwd string) error {
|
||||
expectedHash, ok := h.deps.KV().GetStringIn(UsersNs, user)
|
||||
if !ok {
|
||||
return ErrInvalidConfig
|
||||
}
|
||||
|
||||
return bcrypt.CompareHashAndPassword([]byte(expectedHash), []byte(pwd))
|
||||
}
|
||||
|
||||
func (h *SimpleUserHandlers) Login(c *gin.Context) {
|
||||
req := &LoginReq{}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
@ -96,15 +105,8 @@ func (h *SimpleUserHandlers) Login(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
expectedHash, ok := h.deps.KV().GetStringIn(UsersNs, req.User)
|
||||
if !ok {
|
||||
c.JSON(q.ErrResp(c, 500, ErrInvalidConfig))
|
||||
return
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(expectedHash), []byte(req.Pwd))
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 401, err))
|
||||
if err := h.checkPwd(req.User, req.Pwd); err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -124,32 +126,30 @@ func (h *SimpleUserHandlers) Login(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
hostname := h.cfg.GrabString("Server.Host")
|
||||
secure := h.cfg.GrabBool("Users.CookieSecure")
|
||||
httpOnly := h.cfg.GrabBool("Users.CookieHttpOnly")
|
||||
c.SetCookie(TokenCookie, token, ttl, "/", hostname, secure, httpOnly)
|
||||
c.SetCookie(TokenCookie, token, ttl, "/", "", secure, httpOnly)
|
||||
|
||||
c.JSON(q.Resp(200))
|
||||
}
|
||||
|
||||
type LogoutReq struct {
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
func (h *SimpleUserHandlers) Logout(c *gin.Context) {
|
||||
req := &LogoutReq{}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
|
||||
// token alreay verified in the authn middleware
|
||||
c.SetCookie(TokenCookie, "", 0, "/", "nohost", false, true)
|
||||
secure := h.cfg.GrabBool("Users.CookieSecure")
|
||||
httpOnly := h.cfg.GrabBool("Users.CookieHttpOnly")
|
||||
c.SetCookie(TokenCookie, "", 0, "/", "", secure, httpOnly)
|
||||
c.JSON(q.Resp(200))
|
||||
}
|
||||
|
||||
func (h *SimpleUserHandlers) IsAuthed(c *gin.Context) {
|
||||
// token alreay verified in the authn middleware
|
||||
c.JSON(q.Resp(200))
|
||||
}
|
||||
|
||||
type SetPwdReq struct {
|
||||
User string `json:"user"`
|
||||
OldPwd string `json:"oldPwd"`
|
||||
NewPwd string `json:"newPwd"`
|
||||
}
|
||||
|
@ -159,15 +159,24 @@ func (h *SimpleUserHandlers) SetPwd(c *gin.Context) {
|
|||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(q.ErrResp(c, 400, err))
|
||||
return
|
||||
} else if req.OldPwd == req.NewPwd {
|
||||
c.JSON(q.ErrResp(c, 400, errors.New("password is not updated")))
|
||||
return
|
||||
}
|
||||
|
||||
expectedHash, ok := h.deps.KV().GetStringIn(UsersNs, req.User)
|
||||
claims, err := h.getUserInfo(c)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 401, err))
|
||||
return
|
||||
}
|
||||
|
||||
expectedHash, ok := h.deps.KV().GetStringIn(UsersNs, claims[UserParam])
|
||||
if !ok {
|
||||
c.JSON(q.ErrResp(c, 500, ErrInvalidConfig))
|
||||
return
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(expectedHash), []byte(req.OldPwd))
|
||||
err = bcrypt.CompareHashAndPassword([]byte(expectedHash), []byte(req.OldPwd))
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 401, ErrInvalidUser))
|
||||
return
|
||||
|
@ -178,7 +187,7 @@ func (h *SimpleUserHandlers) SetPwd(c *gin.Context) {
|
|||
c.JSON(q.ErrResp(c, 500, errors.New("fail to set password")))
|
||||
return
|
||||
}
|
||||
err = h.deps.KV().SetStringIn(UsersNs, req.User, string(newHash))
|
||||
err = h.deps.KV().SetStringIn(UsersNs, claims[UserParam], string(newHash))
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, ErrInvalidConfig))
|
||||
return
|
||||
|
@ -186,3 +195,25 @@ func (h *SimpleUserHandlers) SetPwd(c *gin.Context) {
|
|||
|
||||
c.JSON(q.Resp(200))
|
||||
}
|
||||
|
||||
func (h *SimpleUserHandlers) getUserInfo(c *gin.Context) (map[string]string, error) {
|
||||
tokenStr, err := c.Cookie(TokenCookie)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims, err := h.deps.Token().FromToken(
|
||||
tokenStr,
|
||||
map[string]string{
|
||||
UserParam: "",
|
||||
RoleParam: "",
|
||||
ExpireParam: "",
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if claims[UserParam] == "" {
|
||||
return nil, ErrInvalidConfig
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
|
|
@ -15,6 +15,13 @@ var exposedAPIs = map[string]bool{
|
|||
"Health-fm": true,
|
||||
}
|
||||
|
||||
var publicRootPath = "/"
|
||||
var publicStaticPath = "/static"
|
||||
|
||||
func IsPublicPath(accessPath string) bool {
|
||||
return accessPath == publicRootPath || strings.HasPrefix(accessPath, publicStaticPath)
|
||||
}
|
||||
|
||||
func GetHandlerName(fullname string) (string, error) {
|
||||
parts := strings.Split(fullname, ".")
|
||||
if len(parts) == 0 {
|
||||
|
@ -30,13 +37,13 @@ func (h *SimpleUserHandlers) Auth() gin.HandlerFunc {
|
|||
c.JSON(q.ErrResp(c, 401, err))
|
||||
return
|
||||
}
|
||||
accessPath := c.Request.URL.String()
|
||||
|
||||
// TODO: may also check the path
|
||||
enableAuth := h.cfg.GrabBool("Users.EnableAuth")
|
||||
if enableAuth && !exposedAPIs[handlerName] {
|
||||
if enableAuth && !exposedAPIs[handlerName] && !IsPublicPath(accessPath) {
|
||||
token, err := c.Cookie(TokenCookie)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 401, err))
|
||||
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -48,20 +55,20 @@ func (h *SimpleUserHandlers) Auth() gin.HandlerFunc {
|
|||
|
||||
_, err = h.deps.Token().FromToken(token, claims)
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 401, err))
|
||||
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
expire, err := strconv.ParseInt(claims[ExpireParam], 10, 64)
|
||||
if err != nil || expire <= now {
|
||||
c.JSON(q.ErrResp(c, 401, err))
|
||||
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
|
||||
return
|
||||
}
|
||||
|
||||
// visitor is only allowed to download
|
||||
if claims[RoleParam] != AdminRole && handlerName != "Download-fm" {
|
||||
c.JSON(q.Resp(401))
|
||||
c.AbortWithStatusJSON(q.ErrResp(c, 401, errors.New("not allowed")))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue