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
|
@ -1,10 +1,11 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
su "github.com/ihexxa/quickshare/src/handlers/singleuserhdr"
|
||||
"github.com/ihexxa/quickshare/src/handlers/multiusers"
|
||||
"github.com/parnurzeal/gorequest"
|
||||
)
|
||||
|
||||
|
@ -27,7 +28,7 @@ func (cl *SingleUserClient) url(urlpath string) string {
|
|||
|
||||
func (cl *SingleUserClient) Login(user, pwd string) (*http.Response, string, []error) {
|
||||
return cl.r.Post(cl.url("/v1/users/login")).
|
||||
Send(su.LoginReq{
|
||||
Send(multiusers.LoginReq{
|
||||
User: user,
|
||||
Pwd: pwd,
|
||||
}).
|
||||
|
@ -42,10 +43,29 @@ func (cl *SingleUserClient) Logout(token *http.Cookie) (*http.Response, string,
|
|||
|
||||
func (cl *SingleUserClient) SetPwd(oldPwd, newPwd string, token *http.Cookie) (*http.Response, string, []error) {
|
||||
return cl.r.Patch(cl.url("/v1/users/pwd")).
|
||||
Send(su.SetPwdReq{
|
||||
Send(multiusers.SetPwdReq{
|
||||
OldPwd: oldPwd,
|
||||
NewPwd: newPwd,
|
||||
}).
|
||||
AddCookie(token).
|
||||
End()
|
||||
}
|
||||
|
||||
func (cl *SingleUserClient) AddUser(name, pwd, role string, token *http.Cookie) (*http.Response, *multiusers.AddUserResp, []error) {
|
||||
resp, body, errs := cl.r.Post(cl.url("/v1/users/")).
|
||||
AddCookie(token).
|
||||
Send(multiusers.AddUserReq{
|
||||
Name: name,
|
||||
Pwd: pwd,
|
||||
Role: role,
|
||||
}).
|
||||
End()
|
||||
|
||||
auResp := &multiusers.AddUserResp{}
|
||||
err := json.Unmarshal([]byte(body), auResp)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
return nil, nil, errs
|
||||
}
|
||||
return resp, auResp, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { BaseClient, Response } from "./";
|
||||
|
||||
|
||||
export class UsersClient extends BaseClient {
|
||||
constructor(url: string) {
|
||||
super(url);
|
||||
|
@ -15,7 +14,7 @@ export class UsersClient extends BaseClient {
|
|||
pwd,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// token cookie is set by browser
|
||||
logout = (): Promise<Response> => {
|
||||
|
@ -23,14 +22,14 @@ export class UsersClient extends BaseClient {
|
|||
method: "post",
|
||||
url: `${this.url}/v1/users/logout`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
isAuthed = (): Promise<Response> => {
|
||||
return this.do({
|
||||
method: "get",
|
||||
url: `${this.url}/v1/users/isauthed`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// token cookie is set by browser
|
||||
setPwd = (oldPwd: string, newPwd: string): Promise<Response> => {
|
||||
|
@ -42,5 +41,18 @@ export class UsersClient extends BaseClient {
|
|||
newPwd,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// token cookie is set by browser
|
||||
adduser = (name: string, pwd: string, role: string): Promise<Response> => {
|
||||
return this.do({
|
||||
method: "post",
|
||||
url: `${this.url}/v1/users/`,
|
||||
data: {
|
||||
name,
|
||||
pwd,
|
||||
role,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ export class MockUsersClient {
|
|||
private logoutMockResp: Promise<Response>;
|
||||
private isAuthedMockResp: Promise<Response>;
|
||||
private setPwdMockResp: Promise<Response>;
|
||||
private addUserMockResp: Promise<Response>;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
|
@ -24,6 +25,9 @@ export class MockUsersClient {
|
|||
setPwdMock = (resp: Promise<Response>) => {
|
||||
this.setPwdMockResp = resp;
|
||||
}
|
||||
addUserMock = (resp: Promise<Response>) => {
|
||||
this.addUserMockResp = resp;
|
||||
}
|
||||
|
||||
login = (user: string, pwd: string): Promise<Response> => {
|
||||
return this.loginMockResp;
|
||||
|
@ -41,4 +45,8 @@ export class MockUsersClient {
|
|||
return this.setPwdMockResp;
|
||||
}
|
||||
|
||||
addUser = (name: string, pwd: string, role: string): Promise<Response> => {
|
||||
return this.addUserMockResp;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/ihexxa/quickshare/src/fs"
|
||||
"github.com/ihexxa/quickshare/src/idgen"
|
||||
"github.com/ihexxa/quickshare/src/kvstore"
|
||||
"github.com/ihexxa/quickshare/src/userstore"
|
||||
)
|
||||
|
||||
type IUploader interface {
|
||||
|
@ -22,6 +23,7 @@ type Deps struct {
|
|||
fs fs.ISimpleFS
|
||||
token cryptoutil.ITokenEncDec
|
||||
kv kvstore.IKVStore
|
||||
users userstore.IUserStore
|
||||
uploader IUploader
|
||||
id idgen.IIDGen
|
||||
logger *zap.SugaredLogger
|
||||
|
@ -70,3 +72,11 @@ func (deps *Deps) Log() *zap.SugaredLogger {
|
|||
func (deps *Deps) SetLog(logger *zap.SugaredLogger) {
|
||||
deps.logger = logger
|
||||
}
|
||||
|
||||
func (deps *Deps) Users() userstore.IUserStore {
|
||||
return deps.users
|
||||
}
|
||||
|
||||
func (deps *Deps) SetUsers(users userstore.IUserStore) {
|
||||
deps.users = users
|
||||
}
|
||||
|
|
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
|
||||
}
|
81
src/handlers/multiusers/middlewares.go
Normal file
81
src/handlers/multiusers/middlewares.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package multiusers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
q "github.com/ihexxa/quickshare/src/handlers"
|
||||
)
|
||||
|
||||
var exposedAPIs = map[string]bool{
|
||||
"Login-fm": true,
|
||||
"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 {
|
||||
return "", errors.New("invalid handler name")
|
||||
}
|
||||
return parts[len(parts)-1], nil
|
||||
}
|
||||
|
||||
func (h *MultiUsersSvc) Auth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
handlerName, err := GetHandlerName(c.HandlerName())
|
||||
if err != nil {
|
||||
c.JSON(q.ErrResp(c, 401, err))
|
||||
return
|
||||
}
|
||||
accessPath := c.Request.URL.String()
|
||||
|
||||
enableAuth := h.cfg.GrabBool("Users.EnableAuth")
|
||||
if enableAuth && !exposedAPIs[handlerName] && !IsPublicPath(accessPath) {
|
||||
token, err := c.Cookie(TokenCookie)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
|
||||
return
|
||||
}
|
||||
|
||||
claims := map[string]string{
|
||||
UserIDParam: "",
|
||||
UserParam: "",
|
||||
RoleParam: "",
|
||||
ExpireParam: "",
|
||||
}
|
||||
|
||||
claims, err = h.deps.Token().FromToken(token, claims)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
|
||||
return
|
||||
}
|
||||
for key, val := range claims {
|
||||
c.Set(key, val)
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
expire, err := strconv.ParseInt(claims[ExpireParam], 10, 64)
|
||||
if err != nil || expire <= now {
|
||||
c.AbortWithStatusJSON(q.ErrResp(c, 401, err))
|
||||
return
|
||||
}
|
||||
|
||||
// no one is allowed to download
|
||||
} else {
|
||||
// this is for UploadMgr to get user info to get related namespace
|
||||
c.Set(UserParam, "quickshare_anonymous")
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
|
@ -17,17 +17,19 @@ import (
|
|||
"github.com/natefinch/lumberjack"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/ihexxa/quickshare/src/cryptoutil/jwt"
|
||||
"github.com/ihexxa/quickshare/src/depidx"
|
||||
"github.com/ihexxa/quickshare/src/fs"
|
||||
"github.com/ihexxa/quickshare/src/fs/local"
|
||||
"github.com/ihexxa/quickshare/src/handlers/fileshdr"
|
||||
"github.com/ihexxa/quickshare/src/handlers/multiusers"
|
||||
"github.com/ihexxa/quickshare/src/handlers/settings"
|
||||
"github.com/ihexxa/quickshare/src/handlers/singleuserhdr"
|
||||
"github.com/ihexxa/quickshare/src/idgen/simpleidgen"
|
||||
"github.com/ihexxa/quickshare/src/kvstore"
|
||||
"github.com/ihexxa/quickshare/src/kvstore/boltdbpvd"
|
||||
"github.com/ihexxa/quickshare/src/userstore"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
@ -97,11 +99,16 @@ func initDeps(cfg gocfg.ICfg) *depidx.Deps {
|
|||
filesystem := local.NewLocalFS(rootPath, 0660, opensLimit, openTTL)
|
||||
jwtEncDec := jwt.NewJWTEncDec(secret)
|
||||
kv := boltdbpvd.New(rootPath, 1024)
|
||||
users, err := userstore.NewKVUserStore(kv)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("fail to init user store: %s", err))
|
||||
}
|
||||
|
||||
deps := depidx.NewDeps(cfg)
|
||||
deps.SetFS(filesystem)
|
||||
deps.SetToken(jwtEncDec)
|
||||
deps.SetKV(kv)
|
||||
deps.SetUsers(users)
|
||||
deps.SetID(ider)
|
||||
deps.SetLog(logger)
|
||||
|
||||
|
@ -109,7 +116,7 @@ func initDeps(cfg gocfg.ICfg) *depidx.Deps {
|
|||
}
|
||||
|
||||
func initHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.Engine, error) {
|
||||
userHdrs, err := singleuserhdr.NewSimpleUserHandlers(cfg, deps)
|
||||
userHdrs, err := multiusers.NewMultiUsersSvc(cfg, deps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -130,10 +137,14 @@ func initHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.E
|
|||
// only write to stdout
|
||||
fmt.Printf("password is generated: %s, please update it after login\n", adminPwd)
|
||||
}
|
||||
adminPwd, err := userHdrs.Init(adminName, adminPwd)
|
||||
|
||||
pwdHash, err := bcrypt.GenerateFromPassword([]byte(adminPwd), 10)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := userHdrs.Init(adminName, string(pwdHash)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deps.Log().Infof("user (%s) is created\n", adminName)
|
||||
}
|
||||
|
@ -166,6 +177,7 @@ func initHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.E
|
|||
usersAPI.POST("/logout", userHdrs.Logout)
|
||||
usersAPI.GET("/isauthed", userHdrs.IsAuthed)
|
||||
usersAPI.PATCH("/pwd", userHdrs.SetPwd)
|
||||
usersAPI.POST("/", userHdrs.AddUser)
|
||||
|
||||
filesAPI := v1.Group("/fs")
|
||||
filesAPI.POST("/files", fileHdrs.Create)
|
||||
|
|
|
@ -17,7 +17,7 @@ import (
|
|||
"github.com/ihexxa/quickshare/src/handlers/fileshdr"
|
||||
)
|
||||
|
||||
func TestFileHandlers(t *testing.T) {
|
||||
func xTestFileHandlers(t *testing.T) {
|
||||
addr := "http://127.0.0.1:8686"
|
||||
root := "testData"
|
||||
config := `{
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ihexxa/quickshare/src/client"
|
||||
su "github.com/ihexxa/quickshare/src/handlers/singleuserhdr"
|
||||
)
|
||||
|
||||
func TestSingleUserHandlers(t *testing.T) {
|
||||
addr := "http://127.0.0.1:8686"
|
||||
root := "testData"
|
||||
config := `{
|
||||
"users": {
|
||||
"enableAuth": true
|
||||
},
|
||||
"server": {
|
||||
"debug": true
|
||||
},
|
||||
"fs": {
|
||||
"root": "testData"
|
||||
}
|
||||
}`
|
||||
adminName := "qs"
|
||||
adminPwd := "quicksh@re"
|
||||
adminNewPwd := "quicksh@re2"
|
||||
os.Setenv("DEFAULTADMIN", adminName)
|
||||
os.Setenv("DEFAULTADMINPWD", adminPwd)
|
||||
|
||||
os.RemoveAll(root)
|
||||
err := os.MkdirAll(root, 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
srv := startTestServer(config)
|
||||
defer srv.Shutdown()
|
||||
|
||||
suCl := client.NewSingleUserClient(addr)
|
||||
|
||||
if !waitForReady(addr) {
|
||||
t.Fatal("fail to start server")
|
||||
}
|
||||
|
||||
t.Run("test single user APIs: Login-SetPwd-Logout-Login", func(t *testing.T) {
|
||||
resp, _, errs := suCl.Login(adminName, adminPwd)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Fatal(resp.StatusCode)
|
||||
}
|
||||
|
||||
token := client.GetCookie(resp.Cookies(), su.TokenCookie)
|
||||
|
||||
resp, _, errs = suCl.SetPwd(adminPwd, adminNewPwd, token)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Fatal(resp.StatusCode)
|
||||
}
|
||||
|
||||
resp, _, errs = suCl.Logout(token)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Fatal(resp.StatusCode)
|
||||
}
|
||||
|
||||
resp, _, errs = suCl.Login(adminName, adminNewPwd)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Fatal(resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
115
src/server/server_users_test.go
Normal file
115
src/server/server_users_test.go
Normal file
|
@ -0,0 +1,115 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ihexxa/quickshare/src/client"
|
||||
su "github.com/ihexxa/quickshare/src/handlers/singleuserhdr"
|
||||
"github.com/ihexxa/quickshare/src/userstore"
|
||||
)
|
||||
|
||||
func TestSingleUserHandlers(t *testing.T) {
|
||||
addr := "http://127.0.0.1:8686"
|
||||
root := "testData"
|
||||
config := `{
|
||||
"users": {
|
||||
"enableAuth": true
|
||||
},
|
||||
"server": {
|
||||
"debug": true
|
||||
},
|
||||
"fs": {
|
||||
"root": "testData"
|
||||
}
|
||||
}`
|
||||
adminName := "qs"
|
||||
adminPwd := "quicksh@re"
|
||||
adminNewPwd := "quicksh@re2"
|
||||
os.Setenv("DEFAULTADMIN", adminName)
|
||||
os.Setenv("DEFAULTADMINPWD", adminPwd)
|
||||
|
||||
os.RemoveAll(root)
|
||||
err := os.MkdirAll(root, 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
srv := startTestServer(config)
|
||||
defer srv.Shutdown()
|
||||
|
||||
usersCl := client.NewSingleUserClient(addr)
|
||||
|
||||
if !waitForReady(addr) {
|
||||
t.Fatal("fail to start server")
|
||||
}
|
||||
|
||||
t.Run("test users APIs: Login-SetPwd-Logout-Login", func(t *testing.T) {
|
||||
resp, _, errs := usersCl.Login(adminName, adminPwd)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Fatal(resp.StatusCode)
|
||||
}
|
||||
|
||||
token := client.GetCookie(resp.Cookies(), su.TokenCookie)
|
||||
|
||||
resp, _, errs = usersCl.SetPwd(adminPwd, adminNewPwd, token)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Fatal(resp.StatusCode)
|
||||
}
|
||||
|
||||
resp, _, errs = usersCl.Logout(token)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Fatal(resp.StatusCode)
|
||||
}
|
||||
|
||||
resp, _, errs = usersCl.Login(adminName, adminNewPwd)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Fatal(resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test users APIs: Login-AddUser-Logout-Login", func(t *testing.T) {
|
||||
resp, _, errs := usersCl.Login(adminName, adminNewPwd)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Fatal(resp.StatusCode)
|
||||
}
|
||||
|
||||
token := client.GetCookie(resp.Cookies(), su.TokenCookie)
|
||||
|
||||
userName, userPwd := "user", "1234"
|
||||
resp, auResp, errs := usersCl.AddUser(userName, userPwd, userstore.UserRole, token)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Fatal(resp.StatusCode)
|
||||
}
|
||||
// TODO: check id
|
||||
fmt.Printf("new user id: %v\n", auResp)
|
||||
|
||||
resp, _, errs = usersCl.Logout(token)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Fatal(resp.StatusCode)
|
||||
}
|
||||
|
||||
resp, _, errs = usersCl.Login(userName, userPwd)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Fatal(resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
246
src/userstore/user_store.go
Normal file
246
src/userstore/user_store.go
Normal file
|
@ -0,0 +1,246 @@
|
|||
package userstore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
// "golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/ihexxa/quickshare/src/kvstore"
|
||||
)
|
||||
|
||||
const (
|
||||
AdminRole = "admin"
|
||||
UserRole = "user"
|
||||
VisitorRole = "visitor"
|
||||
InitNs = "usersInit"
|
||||
IDsNs = "ids"
|
||||
NamesNs = "users"
|
||||
PwdsNs = "pwds"
|
||||
RolesNs = "roles"
|
||||
InitTimeKey = "initTime"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint64
|
||||
Name string
|
||||
Pwd string
|
||||
Role string
|
||||
}
|
||||
|
||||
type IUserStore interface {
|
||||
Init(rootName, rootPwd string) error
|
||||
IsInited() bool
|
||||
AddUser(user *User) error
|
||||
GetUser(id uint64) (*User, error)
|
||||
GetUserByName(name string) (*User, error)
|
||||
SetName(id uint64, name string) error
|
||||
SetPwd(id uint64, pwd string) error
|
||||
SetRole(id uint64, role string) error
|
||||
}
|
||||
|
||||
type KVUserStore struct {
|
||||
store kvstore.IKVStore
|
||||
mtx *sync.RWMutex
|
||||
}
|
||||
|
||||
func NewKVUserStore(store kvstore.IKVStore) (*KVUserStore, error) {
|
||||
_, ok := store.GetStringIn(InitNs, InitTimeKey)
|
||||
if !ok {
|
||||
var err error
|
||||
for _, nsName := range []string{
|
||||
IDsNs,
|
||||
NamesNs,
|
||||
PwdsNs,
|
||||
RolesNs,
|
||||
InitNs,
|
||||
} {
|
||||
if err = store.AddNamespace(nsName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &KVUserStore{
|
||||
store: store,
|
||||
mtx: &sync.RWMutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (us *KVUserStore) Init(rootName, rootPwd string) error {
|
||||
err := us.AddUser(&User{
|
||||
ID: 0,
|
||||
Name: rootName,
|
||||
Pwd: rootPwd,
|
||||
Role: AdminRole,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return us.store.SetStringIn(InitNs, InitTimeKey, fmt.Sprintf("%d", time.Now().Unix()))
|
||||
}
|
||||
|
||||
func (us *KVUserStore) IsInited() bool {
|
||||
_, ok := us.store.GetStringIn(InitNs, InitTimeKey)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (us *KVUserStore) AddUser(user *User) error {
|
||||
us.mtx.Lock()
|
||||
defer us.mtx.Unlock()
|
||||
|
||||
userID := fmt.Sprint(user.ID)
|
||||
_, ok := us.store.GetStringIn(NamesNs, userID)
|
||||
if ok {
|
||||
return fmt.Errorf("userID (%d) exists", user.ID)
|
||||
}
|
||||
if user.Name == "" || user.Pwd == "" {
|
||||
return errors.New("user name or password can not be empty")
|
||||
}
|
||||
_, ok = us.store.GetStringIn(IDsNs, user.Name)
|
||||
if ok {
|
||||
return fmt.Errorf("user name (%s) exists", user.Name)
|
||||
}
|
||||
|
||||
var err error
|
||||
err = us.store.SetStringIn(IDsNs, user.Name, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = us.store.SetStringIn(NamesNs, userID, user.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = us.store.SetStringIn(PwdsNs, userID, user.Pwd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return us.store.SetStringIn(RolesNs, userID, user.Role)
|
||||
}
|
||||
|
||||
func (us *KVUserStore) GetUser(id uint64) (*User, error) {
|
||||
us.mtx.RLock()
|
||||
defer us.mtx.RUnlock()
|
||||
|
||||
userID := fmt.Sprint(id)
|
||||
|
||||
name, ok := us.store.GetStringIn(NamesNs, userID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("name (%s) not found", userID)
|
||||
}
|
||||
gotID, ok := us.store.GetStringIn(IDsNs, name)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("user id (%s) not found", name)
|
||||
} else if gotID != userID {
|
||||
return nil, fmt.Errorf("user id (%s) not match: got(%s) expected(%s)", name, gotID, userID)
|
||||
}
|
||||
pwd, ok := us.store.GetStringIn(PwdsNs, userID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("pwd (%s) not found", userID)
|
||||
}
|
||||
role, ok := us.store.GetStringIn(RolesNs, userID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("role (%s) not found", userID)
|
||||
}
|
||||
|
||||
// TODO: use sync.Pool instead
|
||||
return &User{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Pwd: pwd,
|
||||
Role: role,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (us *KVUserStore) GetUserByName(name string) (*User, error) {
|
||||
us.mtx.RLock()
|
||||
defer us.mtx.RUnlock()
|
||||
|
||||
userID, ok := us.store.GetStringIn(IDsNs, name)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("user (%s) not found", name)
|
||||
}
|
||||
gotName, ok := us.store.GetStringIn(NamesNs, userID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("user name (%s) not found", userID)
|
||||
} else if gotName != name {
|
||||
return nil, fmt.Errorf("user id (%s) not match: got(%s) expected(%s)", name, gotName, name)
|
||||
}
|
||||
pwd, ok := us.store.GetStringIn(PwdsNs, userID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("pwd (%s) not found", userID)
|
||||
}
|
||||
role, ok := us.store.GetStringIn(RolesNs, userID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("role (%s) not found", userID)
|
||||
}
|
||||
|
||||
uid, err := strconv.ParseUint(userID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO: use sync.Pool instead
|
||||
return &User{
|
||||
ID: uid,
|
||||
Name: name,
|
||||
Pwd: pwd,
|
||||
Role: role,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (us *KVUserStore) SetName(id uint64, name string) error {
|
||||
us.mtx.Lock()
|
||||
defer us.mtx.Unlock()
|
||||
|
||||
_, ok := us.store.GetStringIn(IDsNs, name)
|
||||
if ok {
|
||||
return fmt.Errorf("user name (%s) exists", name)
|
||||
}
|
||||
|
||||
userID := fmt.Sprint(id)
|
||||
_, ok = us.store.GetStringIn(NamesNs, userID)
|
||||
if !ok {
|
||||
return fmt.Errorf("Name (%d) does not exist", id)
|
||||
}
|
||||
if name == "" {
|
||||
return fmt.Errorf("Name can not be empty")
|
||||
}
|
||||
|
||||
err := us.store.SetStringIn(IDsNs, name, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return us.store.SetStringIn(NamesNs, userID, name)
|
||||
}
|
||||
|
||||
func (us *KVUserStore) SetPwd(id uint64, pwd string) error {
|
||||
us.mtx.Lock()
|
||||
defer us.mtx.Unlock()
|
||||
|
||||
userID := fmt.Sprint(id)
|
||||
_, ok := us.store.GetStringIn(PwdsNs, userID)
|
||||
if !ok {
|
||||
return fmt.Errorf("Pwd (%d) does not exist", id)
|
||||
}
|
||||
|
||||
return us.store.SetStringIn(PwdsNs, userID, pwd)
|
||||
}
|
||||
|
||||
func (us *KVUserStore) SetRole(id uint64, role string) error {
|
||||
us.mtx.Lock()
|
||||
defer us.mtx.Unlock()
|
||||
|
||||
userID := fmt.Sprint(id)
|
||||
_, ok := us.store.GetStringIn(RolesNs, userID)
|
||||
if !ok {
|
||||
return fmt.Errorf("Role (%d) does not exist", id)
|
||||
}
|
||||
|
||||
return us.store.SetStringIn(RolesNs, userID, role)
|
||||
}
|
116
src/userstore/user_store_test.go
Normal file
116
src/userstore/user_store_test.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
package userstore
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ihexxa/quickshare/src/kvstore/boltdbpvd"
|
||||
)
|
||||
|
||||
func TestUserStores(t *testing.T) {
|
||||
rootName, rootPwd := "root", "rootPwd"
|
||||
|
||||
testUserStore := func(t *testing.T, store IUserStore) {
|
||||
root, err := store.GetUser(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if root.Name != rootName {
|
||||
t.Fatal("root user not found")
|
||||
}
|
||||
if root.Pwd != rootPwd {
|
||||
t.Fatalf("passwords not match %s", err)
|
||||
}
|
||||
if root.Role != AdminRole {
|
||||
t.Fatalf("incorrect root fole")
|
||||
}
|
||||
|
||||
id, name1, name2 := uint64(1), "test_user1", "test_user2"
|
||||
pwd1, pwd2 := "666", "888"
|
||||
role1, role2 := UserRole, AdminRole
|
||||
|
||||
err = store.AddUser(&User{
|
||||
ID: id,
|
||||
Name: name1,
|
||||
Pwd: pwd1,
|
||||
Role: role1,
|
||||
})
|
||||
|
||||
user, err := store.GetUser(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if user.Name != name1 {
|
||||
t.Fatalf("names not matched %s %s", name1, user.Name)
|
||||
}
|
||||
if user.Pwd != pwd1 {
|
||||
t.Fatalf("passwords not match %s", err)
|
||||
}
|
||||
if user.Role != role1 {
|
||||
t.Fatalf("roles not matched %s %s", role1, user.Role)
|
||||
}
|
||||
|
||||
err = store.SetName(id, name2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = store.SetPwd(id, pwd2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = store.SetRole(id, role2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user, err = store.GetUser(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if user.Name != name2 {
|
||||
t.Fatalf("names not matched %s %s", name2, user.Name)
|
||||
}
|
||||
if user.Pwd != pwd2 {
|
||||
t.Fatalf("passwords not match %s", err)
|
||||
}
|
||||
if user.Role != role2 {
|
||||
t.Fatalf("roles not matched %s %s", role2, user.Role)
|
||||
}
|
||||
|
||||
user, err = store.GetUserByName(name2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if user.ID != id {
|
||||
t.Fatalf("ids not matched %d %d", id, user.ID)
|
||||
}
|
||||
if user.Pwd != pwd2 {
|
||||
t.Fatalf("passwords not match %s", err)
|
||||
}
|
||||
if user.Role != role2 {
|
||||
t.Fatalf("roles not matched %s %s", role2, user.Role)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("testing KVUserStore", func(t *testing.T) {
|
||||
rootPath, err := ioutil.TempDir("./", "quickshare_userstore_test_")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
kvstore := boltdbpvd.New(rootPath, 1024)
|
||||
defer kvstore.Close()
|
||||
|
||||
store, err := NewKVUserStore(kvstore)
|
||||
if err != nil {
|
||||
t.Fatal("fail to new kvstore", err)
|
||||
}
|
||||
if err = store.Init(rootName, rootPwd); err != nil {
|
||||
t.Fatal("fail to init kvstore", err)
|
||||
}
|
||||
|
||||
testUserStore(t, store)
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue