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:
parent
1680c5cb2f
commit
4b6f6d9e1f
13 changed files with 866 additions and 90 deletions
233
src/handlers/multiusers/handlers.go
Normal file
233
src/handlers/multiusers/handlers.go
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue