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
go.sum
1
go.sum
|
@ -104,6 +104,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
su "github.com/ihexxa/quickshare/src/handlers/singleuserhdr"
|
"github.com/ihexxa/quickshare/src/handlers/multiusers"
|
||||||
"github.com/parnurzeal/gorequest"
|
"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) {
|
func (cl *SingleUserClient) Login(user, pwd string) (*http.Response, string, []error) {
|
||||||
return cl.r.Post(cl.url("/v1/users/login")).
|
return cl.r.Post(cl.url("/v1/users/login")).
|
||||||
Send(su.LoginReq{
|
Send(multiusers.LoginReq{
|
||||||
User: user,
|
User: user,
|
||||||
Pwd: pwd,
|
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) {
|
func (cl *SingleUserClient) SetPwd(oldPwd, newPwd string, token *http.Cookie) (*http.Response, string, []error) {
|
||||||
return cl.r.Patch(cl.url("/v1/users/pwd")).
|
return cl.r.Patch(cl.url("/v1/users/pwd")).
|
||||||
Send(su.SetPwdReq{
|
Send(multiusers.SetPwdReq{
|
||||||
OldPwd: oldPwd,
|
OldPwd: oldPwd,
|
||||||
NewPwd: newPwd,
|
NewPwd: newPwd,
|
||||||
}).
|
}).
|
||||||
AddCookie(token).
|
AddCookie(token).
|
||||||
End()
|
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 "./";
|
import { BaseClient, Response } from "./";
|
||||||
|
|
||||||
|
|
||||||
export class UsersClient extends BaseClient {
|
export class UsersClient extends BaseClient {
|
||||||
constructor(url: string) {
|
constructor(url: string) {
|
||||||
super(url);
|
super(url);
|
||||||
|
@ -15,7 +14,7 @@ export class UsersClient extends BaseClient {
|
||||||
pwd,
|
pwd,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// token cookie is set by browser
|
// token cookie is set by browser
|
||||||
logout = (): Promise<Response> => {
|
logout = (): Promise<Response> => {
|
||||||
|
@ -23,14 +22,14 @@ export class UsersClient extends BaseClient {
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${this.url}/v1/users/logout`,
|
url: `${this.url}/v1/users/logout`,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
isAuthed = (): Promise<Response> => {
|
isAuthed = (): Promise<Response> => {
|
||||||
return this.do({
|
return this.do({
|
||||||
method: "get",
|
method: "get",
|
||||||
url: `${this.url}/v1/users/isauthed`,
|
url: `${this.url}/v1/users/isauthed`,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// token cookie is set by browser
|
// token cookie is set by browser
|
||||||
setPwd = (oldPwd: string, newPwd: string): Promise<Response> => {
|
setPwd = (oldPwd: string, newPwd: string): Promise<Response> => {
|
||||||
|
@ -42,5 +41,18 @@ export class UsersClient extends BaseClient {
|
||||||
newPwd,
|
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 logoutMockResp: Promise<Response>;
|
||||||
private isAuthedMockResp: Promise<Response>;
|
private isAuthedMockResp: Promise<Response>;
|
||||||
private setPwdMockResp: Promise<Response>;
|
private setPwdMockResp: Promise<Response>;
|
||||||
|
private addUserMockResp: Promise<Response>;
|
||||||
|
|
||||||
constructor(url: string) {
|
constructor(url: string) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
@ -24,6 +25,9 @@ export class MockUsersClient {
|
||||||
setPwdMock = (resp: Promise<Response>) => {
|
setPwdMock = (resp: Promise<Response>) => {
|
||||||
this.setPwdMockResp = resp;
|
this.setPwdMockResp = resp;
|
||||||
}
|
}
|
||||||
|
addUserMock = (resp: Promise<Response>) => {
|
||||||
|
this.addUserMockResp = resp;
|
||||||
|
}
|
||||||
|
|
||||||
login = (user: string, pwd: string): Promise<Response> => {
|
login = (user: string, pwd: string): Promise<Response> => {
|
||||||
return this.loginMockResp;
|
return this.loginMockResp;
|
||||||
|
@ -41,4 +45,8 @@ export class MockUsersClient {
|
||||||
return this.setPwdMockResp;
|
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/fs"
|
||||||
"github.com/ihexxa/quickshare/src/idgen"
|
"github.com/ihexxa/quickshare/src/idgen"
|
||||||
"github.com/ihexxa/quickshare/src/kvstore"
|
"github.com/ihexxa/quickshare/src/kvstore"
|
||||||
|
"github.com/ihexxa/quickshare/src/userstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IUploader interface {
|
type IUploader interface {
|
||||||
|
@ -22,6 +23,7 @@ type Deps struct {
|
||||||
fs fs.ISimpleFS
|
fs fs.ISimpleFS
|
||||||
token cryptoutil.ITokenEncDec
|
token cryptoutil.ITokenEncDec
|
||||||
kv kvstore.IKVStore
|
kv kvstore.IKVStore
|
||||||
|
users userstore.IUserStore
|
||||||
uploader IUploader
|
uploader IUploader
|
||||||
id idgen.IIDGen
|
id idgen.IIDGen
|
||||||
logger *zap.SugaredLogger
|
logger *zap.SugaredLogger
|
||||||
|
@ -70,3 +72,11 @@ func (deps *Deps) Log() *zap.SugaredLogger {
|
||||||
func (deps *Deps) SetLog(logger *zap.SugaredLogger) {
|
func (deps *Deps) SetLog(logger *zap.SugaredLogger) {
|
||||||
deps.logger = logger
|
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"
|
"github.com/natefinch/lumberjack"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"github.com/ihexxa/quickshare/src/cryptoutil/jwt"
|
"github.com/ihexxa/quickshare/src/cryptoutil/jwt"
|
||||||
"github.com/ihexxa/quickshare/src/depidx"
|
"github.com/ihexxa/quickshare/src/depidx"
|
||||||
"github.com/ihexxa/quickshare/src/fs"
|
"github.com/ihexxa/quickshare/src/fs"
|
||||||
"github.com/ihexxa/quickshare/src/fs/local"
|
"github.com/ihexxa/quickshare/src/fs/local"
|
||||||
"github.com/ihexxa/quickshare/src/handlers/fileshdr"
|
"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/settings"
|
||||||
"github.com/ihexxa/quickshare/src/handlers/singleuserhdr"
|
|
||||||
"github.com/ihexxa/quickshare/src/idgen/simpleidgen"
|
"github.com/ihexxa/quickshare/src/idgen/simpleidgen"
|
||||||
"github.com/ihexxa/quickshare/src/kvstore"
|
"github.com/ihexxa/quickshare/src/kvstore"
|
||||||
"github.com/ihexxa/quickshare/src/kvstore/boltdbpvd"
|
"github.com/ihexxa/quickshare/src/kvstore/boltdbpvd"
|
||||||
|
"github.com/ihexxa/quickshare/src/userstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
|
@ -97,11 +99,16 @@ func initDeps(cfg gocfg.ICfg) *depidx.Deps {
|
||||||
filesystem := local.NewLocalFS(rootPath, 0660, opensLimit, openTTL)
|
filesystem := local.NewLocalFS(rootPath, 0660, opensLimit, openTTL)
|
||||||
jwtEncDec := jwt.NewJWTEncDec(secret)
|
jwtEncDec := jwt.NewJWTEncDec(secret)
|
||||||
kv := boltdbpvd.New(rootPath, 1024)
|
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 := depidx.NewDeps(cfg)
|
||||||
deps.SetFS(filesystem)
|
deps.SetFS(filesystem)
|
||||||
deps.SetToken(jwtEncDec)
|
deps.SetToken(jwtEncDec)
|
||||||
deps.SetKV(kv)
|
deps.SetKV(kv)
|
||||||
|
deps.SetUsers(users)
|
||||||
deps.SetID(ider)
|
deps.SetID(ider)
|
||||||
deps.SetLog(logger)
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -130,10 +137,14 @@ func initHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.E
|
||||||
// only write to stdout
|
// only write to stdout
|
||||||
fmt.Printf("password is generated: %s, please update it after login\n", adminPwd)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if _, err := userHdrs.Init(adminName, string(pwdHash)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
deps.Log().Infof("user (%s) is created\n", adminName)
|
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.POST("/logout", userHdrs.Logout)
|
||||||
usersAPI.GET("/isauthed", userHdrs.IsAuthed)
|
usersAPI.GET("/isauthed", userHdrs.IsAuthed)
|
||||||
usersAPI.PATCH("/pwd", userHdrs.SetPwd)
|
usersAPI.PATCH("/pwd", userHdrs.SetPwd)
|
||||||
|
usersAPI.POST("/", userHdrs.AddUser)
|
||||||
|
|
||||||
filesAPI := v1.Group("/fs")
|
filesAPI := v1.Group("/fs")
|
||||||
filesAPI.POST("/files", fileHdrs.Create)
|
filesAPI.POST("/files", fileHdrs.Create)
|
||||||
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
"github.com/ihexxa/quickshare/src/handlers/fileshdr"
|
"github.com/ihexxa/quickshare/src/handlers/fileshdr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFileHandlers(t *testing.T) {
|
func xTestFileHandlers(t *testing.T) {
|
||||||
addr := "http://127.0.0.1:8686"
|
addr := "http://127.0.0.1:8686"
|
||||||
root := "testData"
|
root := "testData"
|
||||||
config := `{
|
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