feat(server): Replace single-user service with muti-users service (#62)

* feat(svc/multiusers): add multi-users service

* test(multiusers): add unit tests for user store

* feat(multiusers): add multiusers service and refactor userstore

* feat(multiusers): add adduser api and tests

* feat(client): add adduser api
This commit is contained in:
Hexxa 2021-07-10 03:59:59 -05:00 committed by GitHub
parent 1680c5cb2f
commit 4b6f6d9e1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 866 additions and 90 deletions

View file

@ -0,0 +1,233 @@
package multiusers
import (
"errors"
"fmt"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/ihexxa/gocfg"
"golang.org/x/crypto/bcrypt"
"github.com/ihexxa/quickshare/src/depidx"
q "github.com/ihexxa/quickshare/src/handlers"
"github.com/ihexxa/quickshare/src/userstore"
)
var (
ErrInvalidUser = errors.New("invalid user name or password")
ErrInvalidConfig = errors.New("invalid user config")
UserIDParam = "uid"
UserParam = "user"
PwdParam = "pwd"
NewPwdParam = "newpwd"
RoleParam = "role"
ExpireParam = "expire"
TokenCookie = "tk"
)
type MultiUsersSvc struct {
cfg gocfg.ICfg
deps *depidx.Deps
}
func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error) {
return &MultiUsersSvc{
cfg: cfg,
deps: deps,
}, nil
}
func (h *MultiUsersSvc) Init(adminName, adminPwd string) (string, error) {
// TODO: return "" for being compatible with singleuser service, should remove this
err := h.deps.Users().Init(adminName, adminPwd)
return "", err
}
func (h *MultiUsersSvc) IsInited() bool {
return h.deps.Users().IsInited()
}
type LoginReq struct {
User string `json:"user"`
Pwd string `json:"pwd"`
}
func (h *MultiUsersSvc) Login(c *gin.Context) {
req := &LoginReq{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
user, err := h.deps.Users().GetUserByName(req.User)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
err = bcrypt.CompareHashAndPassword([]byte(user.Pwd), []byte(req.Pwd))
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
ttl := h.cfg.GrabInt("Users.CookieTTL")
token, err := h.deps.Token().ToToken(map[string]string{
UserIDParam: fmt.Sprint(user.ID),
UserParam: user.Name,
RoleParam: user.Role,
ExpireParam: fmt.Sprintf("%d", time.Now().Unix()+int64(ttl)),
})
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
secure := h.cfg.GrabBool("Users.CookieSecure")
httpOnly := h.cfg.GrabBool("Users.CookieHttpOnly")
c.SetCookie(TokenCookie, token, ttl, "/", "", secure, httpOnly)
c.JSON(q.Resp(200))
}
type LogoutReq struct{}
func (h *MultiUsersSvc) Logout(c *gin.Context) {
// token alreay verified in the authn middleware
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 *MultiUsersSvc) IsAuthed(c *gin.Context) {
// token alreay verified in the authn middleware
c.JSON(q.Resp(200))
}
type SetPwdReq struct {
OldPwd string `json:"oldPwd"`
NewPwd string `json:"newPwd"`
}
func (h *MultiUsersSvc) SetPwd(c *gin.Context) {
req := &SetPwdReq{}
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
}
claims, err := h.getUserInfo(c)
if err != nil {
c.JSON(q.ErrResp(c, 401, err))
return
}
uid, err := strconv.ParseUint(claims[UserIDParam], 10, 64)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
user, err := h.deps.Users().GetUser(uid)
if err != nil {
c.JSON(q.ErrResp(c, 401, err))
return
}
err = bcrypt.CompareHashAndPassword([]byte(user.Pwd), []byte(req.OldPwd))
if err != nil {
c.JSON(q.ErrResp(c, 401, ErrInvalidUser))
return
}
newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPwd), 10)
if err != nil {
c.JSON(q.ErrResp(c, 500, errors.New("fail to set password")))
return
}
err = h.deps.Users().SetPwd(uid, string(newHash))
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
c.JSON(q.Resp(200))
}
type AddUserReq struct {
Name string `json:"name"`
Pwd string `json:"pwd"`
Role string `json:"role"`
}
type AddUserResp struct {
ID string `json:"id"`
}
func (h *MultiUsersSvc) AddUser(c *gin.Context) {
req := &AddUserReq{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(q.ErrResp(c, 400, err))
return
}
// TODO: check privilege?
// TODO: do more comprehensive validation
// Role and duplicated name will be validated by the store
if len(req.Name) < 2 {
c.JSON(q.ErrResp(c, 400, errors.New("name length must be greater than 2")))
return
} else if len(req.Name) < 3 {
c.JSON(q.ErrResp(c, 400, errors.New("password length must be greater than 2")))
return
}
uid := h.deps.ID().Gen()
pwdHash, err := bcrypt.GenerateFromPassword([]byte(req.Pwd), 10)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
err = h.deps.Users().AddUser(&userstore.User{
ID: uid,
Name: req.Name,
Pwd: string(pwdHash),
Role: req.Role,
})
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
c.JSON(200, &AddUserResp{ID: fmt.Sprint(uid)})
}
func (h *MultiUsersSvc) 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{
UserIDParam: "",
UserParam: "",
RoleParam: "",
ExpireParam: "",
},
)
if err != nil {
return nil, err
} else if claims[UserIDParam] == "" || claims[UserParam] == "" {
return nil, ErrInvalidConfig
}
return claims, nil
}