feat(admin): enable multi-users (#67)
* feat(userstore): support ListUsers * feat(userstore): support del users * feat(multiusers): support list users and delete user apis * feat(client/web): add new apis to web client * fix(ui/panes): move each pane out of the container * feat(ui): add admin pane * feat(users): support force set password api * feat(ui/admin-pane): add functions to admin pane * feat(users): support self API and move uploading folder to home * fix(users): remove home folder when deleting user * fix(ui): remove useless function * feat(ui/panes): hide admin menu if user is not admin * fix(server/files): list home path is incorrect * fix(server): 1.listHome return incorrect cwd 2.addUser init folder with incorrect uid 3.check ns before using * test(server): add regression test cases * test(users, files): add e2e test for concurrent operations * fix(test): clean ups
This commit is contained in:
parent
916ec7c2dc
commit
aefaca98b3
28 changed files with 1562 additions and 478 deletions
|
@ -69,21 +69,26 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.75);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
#panes .container {
|
#panes .root-container {
|
||||||
max-width: 80rem;
|
max-width: 80rem;
|
||||||
width: 96%;
|
width: 96%;
|
||||||
background-color: white;
|
|
||||||
z-index: 101;
|
z-index: 101;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin: 3rem auto 8rem auto;
|
margin: 3rem auto 8rem auto;
|
||||||
border-radius: 0.6rem;
|
border-radius: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#panes .container {
|
||||||
|
background-color: white;
|
||||||
|
margin: 3rem auto 1rem auto;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
#panes .return-btn {
|
#panes .return-btn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
max-width: 960px;
|
max-width: 960px;
|
||||||
|
@ -133,12 +138,17 @@
|
||||||
border-top: solid 1px transparent;
|
border-top: solid 1px transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
#item-list .dot {
|
.container .dot {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#panes .dot {
|
||||||
|
overflow: hidden;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#item-list .vbar {
|
#item-list .vbar {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 1.5rem 1rem;
|
margin: 1.5rem 1rem;
|
||||||
|
@ -164,6 +174,14 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
#item-list .item-op {
|
#item-list .item-op {
|
||||||
line-height: 4rem;
|
line-height: 4rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,6 +130,22 @@ func (cl *FilesClient) List(dirPath string) (*http.Response, *fileshdr.ListResp,
|
||||||
return resp, lResp, nil
|
return resp, lResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cl *FilesClient) ListHome() (*http.Response, *fileshdr.ListResp, []error) {
|
||||||
|
resp, body, errs := cl.r.Get(cl.url("/v1/fs/dirs/home")).
|
||||||
|
AddCookie(cl.token).
|
||||||
|
End()
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return nil, nil, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
lResp := &fileshdr.ListResp{}
|
||||||
|
err := json.Unmarshal([]byte(body), lResp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, append(errs, err)
|
||||||
|
}
|
||||||
|
return resp, lResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (cl *FilesClient) ListUploadings() (*http.Response, *fileshdr.ListUploadingsResp, []error) {
|
func (cl *FilesClient) ListUploadings() (*http.Response, *fileshdr.ListUploadingsResp, []error) {
|
||||||
resp, body, errs := cl.r.Get(cl.url("/v1/fs/uploadings")).
|
resp, body, errs := cl.r.Get(cl.url("/v1/fs/uploadings")).
|
||||||
AddCookie(cl.token).
|
AddCookie(cl.token).
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ihexxa/quickshare/src/handlers"
|
||||||
"github.com/ihexxa/quickshare/src/handlers/multiusers"
|
"github.com/ihexxa/quickshare/src/handlers/multiusers"
|
||||||
"github.com/parnurzeal/gorequest"
|
"github.com/parnurzeal/gorequest"
|
||||||
)
|
)
|
||||||
|
@ -74,6 +75,30 @@ func (cl *SingleUserClient) AddUser(name, pwd, role string, token *http.Cookie)
|
||||||
return resp, auResp, errs
|
return resp, auResp, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cl *SingleUserClient) DelUser(id string, token *http.Cookie) (*http.Response, string, []error) {
|
||||||
|
return cl.r.Delete(cl.url("/v1/users/")).
|
||||||
|
AddCookie(token).
|
||||||
|
Param(handlers.UserIDParam, id).
|
||||||
|
End()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *SingleUserClient) ListUsers(token *http.Cookie) (*http.Response, *multiusers.ListUsersResp, []error) {
|
||||||
|
resp, body, errs := cl.r.Get(cl.url("/v1/users/list")).
|
||||||
|
AddCookie(token).
|
||||||
|
End()
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return nil, nil, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
lsResp := &multiusers.ListUsersResp{}
|
||||||
|
err := json.Unmarshal([]byte(body), lsResp)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
return nil, nil, errs
|
||||||
|
}
|
||||||
|
return resp, lsResp, errs
|
||||||
|
}
|
||||||
|
|
||||||
func (cl *SingleUserClient) AddRole(role string, token *http.Cookie) (*http.Response, string, []error) {
|
func (cl *SingleUserClient) AddRole(role string, token *http.Cookie) (*http.Response, string, []error) {
|
||||||
return cl.r.Post(cl.url("/v1/roles/")).
|
return cl.r.Post(cl.url("/v1/roles/")).
|
||||||
AddCookie(token).
|
AddCookie(token).
|
||||||
|
@ -93,7 +118,7 @@ func (cl *SingleUserClient) DelRole(role string, token *http.Cookie) (*http.Resp
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cl *SingleUserClient) ListRoles(token *http.Cookie) (*http.Response, *multiusers.ListRolesResp, []error) {
|
func (cl *SingleUserClient) ListRoles(token *http.Cookie) (*http.Response, *multiusers.ListRolesResp, []error) {
|
||||||
resp, body, errs := cl.r.Get(cl.url("/v1/roles/")).
|
resp, body, errs := cl.r.Get(cl.url("/v1/roles/list")).
|
||||||
AddCookie(token).
|
AddCookie(token).
|
||||||
End()
|
End()
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
|
@ -108,3 +133,20 @@ func (cl *SingleUserClient) ListRoles(token *http.Cookie) (*http.Response, *mult
|
||||||
}
|
}
|
||||||
return resp, lsResp, errs
|
return resp, lsResp, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cl *SingleUserClient) Self(token *http.Cookie) (*http.Response, *multiusers.SelfResp, []error) {
|
||||||
|
resp, body, errs := cl.r.Get(cl.url("/v1/users/self")).
|
||||||
|
AddCookie(token).
|
||||||
|
End()
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return nil, nil, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
selfResp := &multiusers.SelfResp{}
|
||||||
|
err := json.Unmarshal([]byte(body), selfResp)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
return nil, nil, errs
|
||||||
|
}
|
||||||
|
return resp, selfResp, errs
|
||||||
|
}
|
|
@ -1,12 +1,21 @@
|
||||||
import axios, { AxiosRequestConfig } from "axios";
|
import axios, { AxiosRequestConfig } from "axios";
|
||||||
|
|
||||||
export const defaultTimeout = 10000;
|
export const defaultTimeout = 10000;
|
||||||
|
export const userIDParam = "uid";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
ID: string;
|
id: string;
|
||||||
Name: string;
|
name: string;
|
||||||
Pwd: string;
|
pwd: string;
|
||||||
Role: string;
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListUsersResp {
|
||||||
|
users: Array<User>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListRolesResp {
|
||||||
|
roles: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetadataResp {
|
export interface MetadataResp {
|
||||||
|
@ -42,7 +51,15 @@ export interface IUsersClient {
|
||||||
login: (user: string, pwd: string) => Promise<Response>;
|
login: (user: string, pwd: string) => Promise<Response>;
|
||||||
logout: () => Promise<Response>;
|
logout: () => Promise<Response>;
|
||||||
isAuthed: () => Promise<Response>;
|
isAuthed: () => Promise<Response>;
|
||||||
|
self: () => Promise<Response>;
|
||||||
setPwd: (oldPwd: string, newPwd: string) => Promise<Response>;
|
setPwd: (oldPwd: string, newPwd: string) => Promise<Response>;
|
||||||
|
forceSetPwd: (userID: string, newPwd: string) => Promise<Response>;
|
||||||
|
addUser: (name: string, pwd: string, role: string) => Promise<Response>;
|
||||||
|
delUser: (userID: string) => Promise<Response>;
|
||||||
|
listUsers: () => Promise<Response>;
|
||||||
|
addRole: (role: string) => Promise<Response>;
|
||||||
|
delRole: (role: string) => Promise<Response>;
|
||||||
|
listRoles: () => Promise<Response>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFilesClient {
|
export interface IFilesClient {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { BaseClient, Response } from "./";
|
import { BaseClient, Response, userIDParam } from "./";
|
||||||
|
|
||||||
export class UsersClient extends BaseClient {
|
export class UsersClient extends BaseClient {
|
||||||
constructor(url: string) {
|
constructor(url: string) {
|
||||||
|
@ -16,7 +16,6 @@ export class UsersClient extends BaseClient {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// token cookie is set by browser
|
|
||||||
logout = (): Promise<Response> => {
|
logout = (): Promise<Response> => {
|
||||||
return this.do({
|
return this.do({
|
||||||
method: "post",
|
method: "post",
|
||||||
|
@ -31,7 +30,6 @@ export class UsersClient extends BaseClient {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// token cookie is set by browser
|
|
||||||
setPwd = (oldPwd: string, newPwd: string): Promise<Response> => {
|
setPwd = (oldPwd: string, newPwd: string): Promise<Response> => {
|
||||||
return this.do({
|
return this.do({
|
||||||
method: "patch",
|
method: "patch",
|
||||||
|
@ -43,8 +41,19 @@ export class UsersClient extends BaseClient {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
forceSetPwd = (userID: string, newPwd: string): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "patch",
|
||||||
|
url: `${this.url}/v1/users/pwd/force-set`,
|
||||||
|
data: {
|
||||||
|
id: userID,
|
||||||
|
newPwd,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// token cookie is set by browser
|
// token cookie is set by browser
|
||||||
adduser = (name: string, pwd: string, role: string): Promise<Response> => {
|
addUser = (name: string, pwd: string, role: string): Promise<Response> => {
|
||||||
return this.do({
|
return this.do({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${this.url}/v1/users/`,
|
url: `${this.url}/v1/users/`,
|
||||||
|
@ -55,4 +64,54 @@ export class UsersClient extends BaseClient {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
delUser = (userID: string): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "delete",
|
||||||
|
url: `${this.url}/v1/users/`,
|
||||||
|
params: {
|
||||||
|
[userIDParam]: userID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
listUsers = (): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "get",
|
||||||
|
url: `${this.url}/v1/users/list`,
|
||||||
|
params: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
addRole = (role: string): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "post",
|
||||||
|
url: `${this.url}/v1/roles/`,
|
||||||
|
data: { role },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
delRole = (role: string): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "delete",
|
||||||
|
url: `${this.url}/v1/roles/`,
|
||||||
|
data: { role },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
listRoles = (): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "get",
|
||||||
|
url: `${this.url}/v1/roles/list`,
|
||||||
|
params: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
self = (): Promise<Response> => {
|
||||||
|
return this.do({
|
||||||
|
method: "get",
|
||||||
|
url: `${this.url}/v1/users/self`,
|
||||||
|
params: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,14 @@ 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 forceSetPwdMockResp: Promise<Response>;
|
||||||
private addUserMockResp: Promise<Response>;
|
private addUserMockResp: Promise<Response>;
|
||||||
|
private delUserMockResp: Promise<Response>;
|
||||||
|
private listUsersMockResp: Promise<Response>;
|
||||||
|
private addRoleMockResp: Promise<Response>;
|
||||||
|
private delRoleMockResp: Promise<Response>;
|
||||||
|
private listRolesMockResp: Promise<Response>;
|
||||||
|
private selfMockResp: Promise<Response>;
|
||||||
|
|
||||||
constructor(url: string) {
|
constructor(url: string) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
@ -25,9 +32,30 @@ export class MockUsersClient {
|
||||||
setPwdMock = (resp: Promise<Response>) => {
|
setPwdMock = (resp: Promise<Response>) => {
|
||||||
this.setPwdMockResp = resp;
|
this.setPwdMockResp = resp;
|
||||||
}
|
}
|
||||||
|
forceSetPwdMock = (resp: Promise<Response>) => {
|
||||||
|
this.forceSetPwdMockResp = resp;
|
||||||
|
}
|
||||||
addUserMock = (resp: Promise<Response>) => {
|
addUserMock = (resp: Promise<Response>) => {
|
||||||
this.addUserMockResp = resp;
|
this.addUserMockResp = resp;
|
||||||
}
|
}
|
||||||
|
delUserMock = (resp: Promise<Response>) => {
|
||||||
|
this.delUserMockResp = resp;
|
||||||
|
}
|
||||||
|
listUsersMock = (resp: Promise<Response>) => {
|
||||||
|
this.listUsersMockResp = resp;
|
||||||
|
}
|
||||||
|
addRoleMock = (resp: Promise<Response>) => {
|
||||||
|
this.addRoleMockResp = resp;
|
||||||
|
}
|
||||||
|
delRoleMock = (resp: Promise<Response>) => {
|
||||||
|
this.delRoleMockResp = resp;
|
||||||
|
}
|
||||||
|
listRolesMock = (resp: Promise<Response>) => {
|
||||||
|
this.listRolesMockResp = resp;
|
||||||
|
}
|
||||||
|
slefMock = (resp: Promise<Response>) => {
|
||||||
|
this.selfMockResp = resp;
|
||||||
|
}
|
||||||
|
|
||||||
login = (user: string, pwd: string): Promise<Response> => {
|
login = (user: string, pwd: string): Promise<Response> => {
|
||||||
return this.loginMockResp;
|
return this.loginMockResp;
|
||||||
|
@ -45,8 +73,35 @@ export class MockUsersClient {
|
||||||
return this.setPwdMockResp;
|
return this.setPwdMockResp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
forceSetPwd = (userID: string, newPwd: string): Promise<Response> => {
|
||||||
|
return this.forceSetPwdMockResp;
|
||||||
|
}
|
||||||
|
|
||||||
addUser = (name: string, pwd: string, role: string): Promise<Response> => {
|
addUser = (name: string, pwd: string, role: string): Promise<Response> => {
|
||||||
return this.addUserMockResp;
|
return this.addUserMockResp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delUser = (userID: string): Promise<Response> => {
|
||||||
|
return this.delUserMockResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
listUsers = (): Promise<Response> => {
|
||||||
|
return this.listUsersMockResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
addRole = (role: string): Promise<Response> => {
|
||||||
|
return this.addRoleMockResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
delRole = (role: string): Promise<Response> => {
|
||||||
|
return this.delRoleMockResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
listRoles = (): Promise<Response> => {
|
||||||
|
return this.listRolesMockResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
self = (): Promise<Response> => {
|
||||||
|
return this.selfMockResp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -259,52 +259,6 @@ export class Browser extends React.Component<Props, State, {}> {
|
||||||
const sizeCellClass = this.props.isVertical ? `hidden margin-s` : ``;
|
const sizeCellClass = this.props.isVertical ? `hidden margin-s` : ``;
|
||||||
const modTimeCellClass = this.props.isVertical ? `hidden margin-s` : ``;
|
const modTimeCellClass = this.props.isVertical ? `hidden margin-s` : ``;
|
||||||
|
|
||||||
const layoutChildren = [
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => this.delete()}
|
|
||||||
className="red0-bg white-font margin-t-m margin-b-m"
|
|
||||||
>
|
|
||||||
Delete Selected
|
|
||||||
</button>,
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => this.moveHere()}
|
|
||||||
className="grey1-bg white-font margin-t-m margin-b-m"
|
|
||||||
>
|
|
||||||
Paste
|
|
||||||
</button>,
|
|
||||||
<span className="inline-block margin-t-m margin-b-m">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
value={this.state.inputValue}
|
|
||||||
className="black0-font margin-r-m"
|
|
||||||
placeholder="folder name"
|
|
||||||
/>
|
|
||||||
<button onClick={this.onMkDir} className="grey1-bg white-font">
|
|
||||||
Create Folder
|
|
||||||
</button>
|
|
||||||
</span>,
|
|
||||||
<span className="inline-block margin-t-m margin-b-m">
|
|
||||||
<button onClick={this.onClickUpload} className="green0-bg white-font">
|
|
||||||
Upload Files
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
onChange={this.addUploads}
|
|
||||||
multiple={true}
|
|
||||||
value={this.props.uploadValue}
|
|
||||||
ref={this.assignInput}
|
|
||||||
className="black0-font hidden"
|
|
||||||
/>
|
|
||||||
</span>,
|
|
||||||
];
|
|
||||||
|
|
||||||
// const ops = (
|
|
||||||
// <Layouter isHorizontal={false} elements={layoutChildren}></Layouter>
|
|
||||||
// );
|
|
||||||
|
|
||||||
const ops = (
|
const ops = (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -124,17 +124,6 @@ export class Updater {
|
||||||
: this.props.items;
|
: this.props.items;
|
||||||
};
|
};
|
||||||
|
|
||||||
goHome = async (): Promise<void> => {
|
|
||||||
const listResp = await this.filesClient.listHome();
|
|
||||||
|
|
||||||
// how to get current dir? to dirPath?
|
|
||||||
// this.props.dirPath = dirParts;
|
|
||||||
this.props.items =
|
|
||||||
listResp.status === 200
|
|
||||||
? List<MetadataResp>(listResp.data.metadatas)
|
|
||||||
: this.props.items;
|
|
||||||
};
|
|
||||||
|
|
||||||
moveHere = async (
|
moveHere = async (
|
||||||
srcDir: string,
|
srcDir: string,
|
||||||
dstDir: string,
|
dstDir: string,
|
||||||
|
|
|
@ -60,16 +60,17 @@ export function initState(): ICoreState {
|
||||||
uploadFiles: List<File>([]),
|
uploadFiles: List<File>([]),
|
||||||
},
|
},
|
||||||
panes: {
|
panes: {
|
||||||
|
userRole: "",
|
||||||
displaying: "",
|
displaying: "",
|
||||||
paneNames: Set<string>(["settings", "login"]),
|
paneNames: Set<string>(["settings", "login", "admin"]),
|
||||||
login: {
|
login: {
|
||||||
authed: false,
|
authed: false,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
admin: {
|
admin: {
|
||||||
users: Map<string, User>(),
|
users: Map<string, User>(),
|
||||||
roles: Set<string>()
|
roles: Set<string>(),
|
||||||
}
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -92,16 +93,17 @@ export function mockState(): ICoreState {
|
||||||
uploadFiles: List<File>([]),
|
uploadFiles: List<File>([]),
|
||||||
},
|
},
|
||||||
panes: {
|
panes: {
|
||||||
|
userRole: "",
|
||||||
displaying: "",
|
displaying: "",
|
||||||
paneNames: Set<string>(["settings", "login"]),
|
paneNames: Set<string>(["settings", "login", "admin"]),
|
||||||
login: {
|
login: {
|
||||||
authed: false,
|
authed: false,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
admin: {
|
admin: {
|
||||||
users: Map<string, User>(),
|
users: Map<string, User>(),
|
||||||
roles: Set<string>()
|
roles: Set<string>(),
|
||||||
}
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,8 @@ import * as React from "react";
|
||||||
import { Map, Set } from "immutable";
|
import { Map, Set } from "immutable";
|
||||||
|
|
||||||
import { ICoreState } from "./core_state";
|
import { ICoreState } from "./core_state";
|
||||||
import { IUsersClient, User} from "../client";
|
import { User } from "../client";
|
||||||
import { UsersClient } from "../client/users";
|
|
||||||
import { Updater as PanesUpdater } from "./panes";
|
import { Updater as PanesUpdater } from "./panes";
|
||||||
import { updater as BrowserUpdater } from "./browser.updater";
|
|
||||||
import { Layouter } from "./layouter";
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
users: Map<string, User>;
|
users: Map<string, User>;
|
||||||
|
@ -14,165 +11,408 @@ export interface Props {
|
||||||
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Updater {
|
export interface UserFormProps {
|
||||||
private static props: Props;
|
key: string;
|
||||||
private static client: IUsersClient;
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
roles: Set<string>;
|
||||||
|
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||||
|
}
|
||||||
|
|
||||||
static init = (props: Props) => (Updater.props = { ...props });
|
export interface UserFormState {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
newPwd1: string;
|
||||||
|
newPwd2: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
static setClient = (client: IUsersClient): void => {
|
export class UserForm extends React.Component<
|
||||||
Updater.client = client;
|
UserFormProps,
|
||||||
};
|
UserFormState,
|
||||||
|
{}
|
||||||
// static adduser = async (user: User): Promise<boolean> => {
|
> {
|
||||||
// const resp = await Updater.client.add
|
constructor(p: UserFormProps) {
|
||||||
// }
|
super(p);
|
||||||
|
this.state = {
|
||||||
// static login = async (user: string, pwd: string): Promise<boolean> => {
|
id: p.id,
|
||||||
// const resp = await Updater.client.login(user, pwd);
|
name: p.name,
|
||||||
// Updater.setAuthed(resp.status === 200);
|
newPwd1: "",
|
||||||
// return resp.status === 200;
|
newPwd2: "",
|
||||||
// };
|
role: p.role,
|
||||||
|
|
||||||
// static logout = async (): Promise<boolean> => {
|
|
||||||
// const resp = await Updater.client.logout();
|
|
||||||
// Updater.setAuthed(false);
|
|
||||||
// return resp.status === 200;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// static isAuthed = async (): Promise<boolean> => {
|
|
||||||
// const resp = await Updater.client.isAuthed();
|
|
||||||
// return resp.status === 200;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// static initIsAuthed = async (): Promise<void> => {
|
|
||||||
// return Updater.isAuthed().then((isAuthed) => {
|
|
||||||
// Updater.setAuthed(isAuthed);
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
static setState = (preState: ICoreState): ICoreState => {
|
|
||||||
preState.panel.authPane = {
|
|
||||||
...preState.panel.authPane,
|
|
||||||
...Updater.props,
|
|
||||||
};
|
|
||||||
return preState;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// export interface State {
|
changePwd1 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
// user: string;
|
this.setState({ newPwd1: ev.target.value });
|
||||||
// pwd: string;
|
};
|
||||||
// }
|
changePwd2 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ newPwd2: ev.target.value });
|
||||||
|
};
|
||||||
|
changeRole = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ role: ev.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
// export class AuthPane extends React.Component<Props, State, {}> {
|
setPwd = () => {
|
||||||
// private update: (updater: (prevState: ICoreState) => ICoreState) => void;
|
if (this.state.newPwd1 !== this.state.newPwd2) {
|
||||||
// constructor(p: Props) {
|
alert("2 passwords do not match, please check.");
|
||||||
// super(p);
|
return;
|
||||||
// Updater.init(p);
|
}
|
||||||
// Updater.setClient(new UsersClient(""));
|
|
||||||
// this.update = p.update;
|
|
||||||
// this.state = {
|
|
||||||
// user: "",
|
|
||||||
// pwd: "",
|
|
||||||
// };
|
|
||||||
|
|
||||||
// this.initIsAuthed();
|
PanesUpdater.forceSetPwd(this.state.id, this.state.newPwd1).then(
|
||||||
// }
|
(ok: boolean) => {
|
||||||
|
if (ok) {
|
||||||
|
alert("password is updated");
|
||||||
|
} else {
|
||||||
|
alert("failed to update password");
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
newPwd1: "",
|
||||||
|
newPwd2: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// changeUser = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
delUser = () => {
|
||||||
// this.setState({ user: ev.target.value });
|
PanesUpdater.delUser(this.state.id)
|
||||||
// };
|
.then((ok: boolean) => {
|
||||||
|
if (!ok) {
|
||||||
|
alert("failed to delete user");
|
||||||
|
}
|
||||||
|
return PanesUpdater.listUsers();
|
||||||
|
})
|
||||||
|
.then((_: boolean) => {
|
||||||
|
this.props.update(PanesUpdater.updateState);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// changePwd = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
// setRole = () => {};
|
||||||
// this.setState({ pwd: ev.target.value });
|
|
||||||
// };
|
|
||||||
|
|
||||||
// initIsAuthed = () => {
|
render() {
|
||||||
// Updater.initIsAuthed().then(() => {
|
return (
|
||||||
// this.update(Updater.setAuthPane);
|
<span>
|
||||||
// });
|
<span className="flex-list-container">
|
||||||
// };
|
<div className="flex-list-item-l">
|
||||||
|
<span className="vbar green0-bg"></span>
|
||||||
|
<div
|
||||||
|
className="margin-l-m"
|
||||||
|
style={{
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="bold item-name">Name: {this.props.name}</div>
|
||||||
|
<div className="grey1-font item-name">
|
||||||
|
ID: {this.props.id} / Role: {this.props.role}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex-list-item-r"
|
||||||
|
style={{
|
||||||
|
flexDirection: "column",
|
||||||
|
flexBasis: "80%",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="margin-t-m">
|
||||||
|
<button
|
||||||
|
onClick={this.delUser}
|
||||||
|
className="grey1-bg white-font margin-r-m"
|
||||||
|
>
|
||||||
|
Delete User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
// login = () => {
|
{/* no API yet */}
|
||||||
// Updater.login(this.state.user, this.state.pwd)
|
{/* <div className="margin-t-m">
|
||||||
// .then((ok: boolean) => {
|
<input
|
||||||
// if (ok) {
|
name={`${this.props.id}-role`}
|
||||||
// this.update(Updater.setAuthPane);
|
type="text"
|
||||||
// this.setState({ user: "", pwd: "" });
|
onChange={this.changeRole}
|
||||||
// // close all the panes
|
value={this.state.role}
|
||||||
// PanesUpdater.displayPane("");
|
className="black0-font margin-r-m"
|
||||||
// this.update(PanesUpdater.updateState);
|
placeholder={this.props.role}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={this.setRole}
|
||||||
|
className="grey1-bg white-font margin-r-m"
|
||||||
|
>
|
||||||
|
Update Role
|
||||||
|
</button>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
// // refresh
|
<div className="margin-t-m">
|
||||||
// return BrowserUpdater().setHomeItems();
|
<input
|
||||||
// } else {
|
name={`${this.props.id}-pwd1`}
|
||||||
// this.setState({ user: "", pwd: "" });
|
type="password"
|
||||||
// alert("Failed to login.");
|
onChange={this.changePwd1}
|
||||||
// }
|
value={this.state.newPwd1}
|
||||||
// })
|
className="black0-font margin-r-m"
|
||||||
// .then(() => {
|
placeholder="new password"
|
||||||
// return BrowserUpdater().refreshUploadings();
|
/>
|
||||||
// })
|
<input
|
||||||
// .then((_: boolean) => {
|
name={`${this.props.id}-pwd2`}
|
||||||
// this.update(BrowserUpdater().setBrowser);
|
type="password"
|
||||||
// });
|
onChange={this.changePwd2}
|
||||||
// };
|
value={this.state.newPwd2}
|
||||||
|
className="black0-font margin-r-m"
|
||||||
|
placeholder="repeat password"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={this.setPwd}
|
||||||
|
className="grey1-bg white-font margin-r-m"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
// logout = () => {
|
</span>
|
||||||
// Updater.logout().then((ok: boolean) => {
|
);
|
||||||
// if (ok) {
|
}
|
||||||
// this.update(Updater.setAuthPane);
|
}
|
||||||
// } else {
|
|
||||||
// alert("Failed to logout.");
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
// render() {
|
export interface State {
|
||||||
// const elements: Array<JSX.Element> = [
|
newUserName: string;
|
||||||
// <input
|
newUserPwd1: string;
|
||||||
// name="user"
|
newUserPwd2: string;
|
||||||
// type="text"
|
newUserRole: string;
|
||||||
// onChange={this.changeUser}
|
newRole: string;
|
||||||
// value={this.state.user}
|
}
|
||||||
// className="black0-font margin-t-m margin-b-m"
|
export class AdminPane extends React.Component<Props, State, {}> {
|
||||||
// // style={{ width: "80%" }}
|
constructor(p: Props) {
|
||||||
// placeholder="user name"
|
super(p);
|
||||||
// />,
|
this.state = {
|
||||||
// <input
|
newUserName: "",
|
||||||
// name="pwd"
|
newUserPwd1: "",
|
||||||
// type="password"
|
newUserPwd2: "",
|
||||||
// onChange={this.changePwd}
|
newUserRole: "",
|
||||||
// value={this.state.pwd}
|
newRole: "",
|
||||||
// className="black0-font margin-t-m margin-b-m"
|
};
|
||||||
// // style={{ width: "80%" }}
|
}
|
||||||
// placeholder="password"
|
|
||||||
// />,
|
|
||||||
// <button
|
|
||||||
// onClick={this.login}
|
|
||||||
// className="green0-bg white-font margin-t-m margin-b-m"
|
|
||||||
// >
|
|
||||||
// Log in
|
|
||||||
// </button>,
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// return (
|
onChangeUserName = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
// <span>
|
this.setState({ newUserName: ev.target.value });
|
||||||
// <div
|
};
|
||||||
// className="margin-l-l"
|
onChangeUserPwd1 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
// style={{ display: this.props.authed ? "none" : "block" }}
|
this.setState({ newUserPwd1: ev.target.value });
|
||||||
// >
|
};
|
||||||
// {/* <h5 className="black-font">Login</h5> */}
|
onChangeUserPwd2 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
// <Layouter isHorizontal={false} elements={elements} />
|
this.setState({ newUserPwd2: ev.target.value });
|
||||||
// </div>
|
};
|
||||||
|
onChangeUserRole = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ newUserRole: ev.target.value });
|
||||||
|
};
|
||||||
|
onChangeRole = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ newRole: ev.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
// <span style={{ display: this.props.authed ? "inherit" : "none" }}>
|
addRole = () => {
|
||||||
// <button onClick={this.logout} className="grey1-bg white-font">
|
PanesUpdater.addRole(this.state.newRole)
|
||||||
// Log out
|
.then((ok: boolean) => {
|
||||||
// </button>
|
if (!ok) {
|
||||||
// </span>
|
alert("failed to add role");
|
||||||
// </span>
|
}
|
||||||
// );
|
return PanesUpdater.listRoles();
|
||||||
// }
|
})
|
||||||
// }
|
.then(() => {
|
||||||
|
this.props.update(PanesUpdater.updateState);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
delRole = (role: string) => {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
"After deleting this role, some of users may not be able to login."
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PanesUpdater.delRole(role)
|
||||||
|
.then((ok: boolean) => {
|
||||||
|
if (!ok) {
|
||||||
|
alert("failed to delete role");
|
||||||
|
}
|
||||||
|
return PanesUpdater.listRoles();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.props.update(PanesUpdater.updateState);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
addUser = () => {
|
||||||
|
if (this.state.newUserPwd1 !== this.state.newUserPwd2) {
|
||||||
|
alert("2 passwords do not match, please check.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PanesUpdater.addUser({
|
||||||
|
id: "", // backend will fill it
|
||||||
|
name: this.state.newUserName,
|
||||||
|
pwd: this.state.newUserPwd1,
|
||||||
|
role: this.state.newUserRole,
|
||||||
|
})
|
||||||
|
.then((ok: boolean) => {
|
||||||
|
if (!ok) {
|
||||||
|
alert("failed to add user");
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
newUserName: "",
|
||||||
|
newUserPwd1: "",
|
||||||
|
newUserPwd2: "",
|
||||||
|
newUserRole: "",
|
||||||
|
});
|
||||||
|
return PanesUpdater.listUsers();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.props.update(PanesUpdater.updateState);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const userList = this.props.users.valueSeq().map((user: User) => {
|
||||||
|
return (
|
||||||
|
<div className="margin-t-m">
|
||||||
|
<UserForm
|
||||||
|
key={user.id}
|
||||||
|
id={user.id}
|
||||||
|
name={user.name}
|
||||||
|
role={user.role}
|
||||||
|
roles={this.props.roles}
|
||||||
|
update={this.props.update}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const roleList = this.props.roles.valueSeq().map((role: string) => {
|
||||||
|
return (
|
||||||
|
<div key={role} className="flex-list-container margin-b-m">
|
||||||
|
<div className="flex-list-item-l">
|
||||||
|
<span className="dot red0-bg"></span>
|
||||||
|
<span className="bold">{role}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-list-item-r">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
this.delRole(role);
|
||||||
|
}}
|
||||||
|
className="grey1-bg white-font margin-r-m"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="font-size-m">
|
||||||
|
<div className="container">
|
||||||
|
<div className="flex-list-container padding-l">
|
||||||
|
{/* <span className="inline-block margin-t-m margin-b-m"> */}
|
||||||
|
<div
|
||||||
|
className="flex-list-item-l"
|
||||||
|
style={{
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={this.onChangeUserName}
|
||||||
|
value={this.state.newUserName}
|
||||||
|
className="black0-font margin-b-m"
|
||||||
|
placeholder="new user name"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={this.onChangeUserRole}
|
||||||
|
value={this.state.newUserRole}
|
||||||
|
className="black0-font margin-b-m"
|
||||||
|
placeholder="new user role"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
onChange={this.onChangeUserPwd1}
|
||||||
|
value={this.state.newUserPwd1}
|
||||||
|
className="black0-font margin-b-m"
|
||||||
|
placeholder="password"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
onChange={this.onChangeUserPwd2}
|
||||||
|
value={this.state.newUserPwd2}
|
||||||
|
className="black0-font margin-b-m"
|
||||||
|
placeholder="repeat password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-list-item-r">
|
||||||
|
<button
|
||||||
|
onClick={this.addUser}
|
||||||
|
className="grey1-bg white-font margin-r-m"
|
||||||
|
>
|
||||||
|
Create User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* </span> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container">
|
||||||
|
<div className="padding-l">
|
||||||
|
<div className="flex-list-container bold">
|
||||||
|
<span className="flex-list-item-l">
|
||||||
|
<span className="dot black-bg"></span>
|
||||||
|
<span>Users</span>
|
||||||
|
</span>
|
||||||
|
<span className="flex-list-item-r padding-r-m"></span>
|
||||||
|
</div>
|
||||||
|
{userList}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container">
|
||||||
|
<div className="flex-list-container padding-l">
|
||||||
|
<div className="flex-list-item-l">
|
||||||
|
<span className="inline-block margin-t-m margin-b-m">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={this.onChangeRole}
|
||||||
|
value={this.state.newRole}
|
||||||
|
className="black0-font margin-r-m"
|
||||||
|
placeholder="new role name"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-list-item-r">
|
||||||
|
<button
|
||||||
|
onClick={this.addRole}
|
||||||
|
className="grey1-bg white-font margin-r-m"
|
||||||
|
>
|
||||||
|
Create Role
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container">
|
||||||
|
<div className="padding-l">
|
||||||
|
<div className="flex-list-container bold margin-b-m">
|
||||||
|
<span className="flex-list-item-l">
|
||||||
|
<span className="dot black-bg"></span>
|
||||||
|
<span>Roles</span>
|
||||||
|
</span>
|
||||||
|
<span className="flex-list-item-r padding-r-m"></span>
|
||||||
|
</div>
|
||||||
|
{roleList}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -159,12 +159,14 @@ export class AuthPane extends React.Component<Props, State, {}> {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<div
|
<div
|
||||||
className="margin-l-l"
|
className="container"
|
||||||
style={{ display: this.props.authed ? "none" : "block" }}
|
style={{ display: this.props.authed ? "none" : "block" }}
|
||||||
>
|
>
|
||||||
|
<div className="padding-l">
|
||||||
{/* <h5 className="black-font">Login</h5> */}
|
{/* <h5 className="black-font">Login</h5> */}
|
||||||
<Layouter isHorizontal={false} elements={elements} />
|
<Layouter isHorizontal={false} elements={elements} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span style={{ display: this.props.authed ? "inherit" : "none" }}>
|
<span style={{ display: this.props.authed ? "inherit" : "none" }}>
|
||||||
<button onClick={this.logout} className="grey1-bg white-font">
|
<button onClick={this.logout} className="grey1-bg white-font">
|
||||||
|
|
|
@ -120,6 +120,7 @@ export class PaneSettings extends React.Component<Props, State, {}> {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="container">
|
||||||
<div className="padding-l">
|
<div className="padding-l">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex-list-container">
|
<div className="flex-list-container">
|
||||||
|
@ -171,7 +172,11 @@ export class PaneSettings extends React.Component<Props, State, {}> {
|
||||||
<h5 className="black-font">Logout</h5>
|
<h5 className="black-font">Logout</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-list-item-r">
|
<div className="flex-list-item-r">
|
||||||
<AuthPane authed={this.props.login.authed} update={this.update} />
|
<AuthPane
|
||||||
|
authed={this.props.login.authed}
|
||||||
|
update={this.update}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,21 +1,30 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Set, Map } from "immutable";
|
import { Set, Map } from "immutable";
|
||||||
|
|
||||||
|
import { IUsersClient, User, ListUsersResp, ListRolesResp } from "../client";
|
||||||
|
import { UsersClient } from "../client/users";
|
||||||
import { ICoreState } from "./core_state";
|
import { ICoreState } from "./core_state";
|
||||||
import { PaneSettings } from "./pane_settings";
|
import { PaneSettings } from "./pane_settings";
|
||||||
|
import { AdminPane, Props as AdminPaneProps } from "./pane_admin";
|
||||||
import { AuthPane, Props as AuthPaneProps } from "./pane_login";
|
import { AuthPane, Props as AuthPaneProps } from "./pane_login";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
userRole: string;
|
||||||
displaying: string;
|
displaying: string;
|
||||||
paneNames: Set<string>;
|
paneNames: Set<string>;
|
||||||
login: AuthPaneProps;
|
login: AuthPaneProps;
|
||||||
|
admin: AdminPaneProps;
|
||||||
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Updater {
|
export class Updater {
|
||||||
static props: Props;
|
static props: Props;
|
||||||
|
private static client: IUsersClient;
|
||||||
|
|
||||||
static init = (props: Props) => (Updater.props = { ...props });
|
static init = (props: Props) => (Updater.props = { ...props });
|
||||||
|
static setClient = (client: IUsersClient): void => {
|
||||||
|
Updater.client = client;
|
||||||
|
};
|
||||||
|
|
||||||
static displayPane = (paneName: string) => {
|
static displayPane = (paneName: string) => {
|
||||||
if (paneName === "") {
|
if (paneName === "") {
|
||||||
|
@ -31,7 +40,85 @@ export class Updater {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static self = async (): Promise<boolean> => {
|
||||||
|
const resp = await Updater.client.self();
|
||||||
|
if (resp.status === 200) {
|
||||||
|
Updater.props.userRole = resp.data.role;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static addUser = async (user: User): Promise<boolean> => {
|
||||||
|
const resp = await Updater.client.addUser(user.name, user.pwd, user.role);
|
||||||
|
// TODO: should return uid instead
|
||||||
|
return resp.status === 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
static delUser = async (userID: string): Promise<boolean> => {
|
||||||
|
const resp = await Updater.client.delUser(userID);
|
||||||
|
return resp.status === 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
static setRole = async (userID: string, role: string): Promise<boolean> => {
|
||||||
|
const resp = await Updater.client.delUser(userID);
|
||||||
|
return resp.status === 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
static forceSetPwd = async (
|
||||||
|
userID: string,
|
||||||
|
pwd: string
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const resp = await Updater.client.forceSetPwd(userID, pwd);
|
||||||
|
return resp.status === 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
static listUsers = async (): Promise<boolean> => {
|
||||||
|
const resp = await Updater.client.listUsers();
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lsRes = resp.data as ListUsersResp;
|
||||||
|
let users = Map<User>({});
|
||||||
|
lsRes.users.forEach((user: User) => {
|
||||||
|
users = users.set(user.name, user);
|
||||||
|
});
|
||||||
|
Updater.props.admin.users = users;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
static addRole = async (role: string): Promise<boolean> => {
|
||||||
|
const resp = await Updater.client.addRole(role);
|
||||||
|
// TODO: should return uid instead
|
||||||
|
return resp.status === 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
static delRole = async (role: string): Promise<boolean> => {
|
||||||
|
const resp = await Updater.client.delRole(role);
|
||||||
|
return resp.status === 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
static listRoles = async (): Promise<boolean> => {
|
||||||
|
const resp = await Updater.client.listRoles();
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lsRes = resp.data as ListRolesResp;
|
||||||
|
let roles = Set<string>();
|
||||||
|
Object.keys(lsRes.roles).forEach((role: string) => {
|
||||||
|
roles = roles.add(role);
|
||||||
|
});
|
||||||
|
Updater.props.admin.roles = roles;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
static updateState = (prevState: ICoreState): ICoreState => {
|
static updateState = (prevState: ICoreState): ICoreState => {
|
||||||
|
console.log(prevState, Updater.props);
|
||||||
return {
|
return {
|
||||||
...prevState,
|
...prevState,
|
||||||
panel: {
|
panel: {
|
||||||
|
@ -47,6 +134,7 @@ export class Panes extends React.Component<Props, State, {}> {
|
||||||
constructor(p: Props) {
|
constructor(p: Props) {
|
||||||
super(p);
|
super(p);
|
||||||
Updater.init(p);
|
Updater.init(p);
|
||||||
|
Updater.setClient(new UsersClient(""));
|
||||||
}
|
}
|
||||||
|
|
||||||
closePane = () => {
|
closePane = () => {
|
||||||
|
@ -63,7 +151,7 @@ export class Panes extends React.Component<Props, State, {}> {
|
||||||
displaying = "login";
|
displaying = "login";
|
||||||
}
|
}
|
||||||
|
|
||||||
const panesMap: Map<string, JSX.Element> = Map({
|
let panesMap: Map<string, JSX.Element> = Map({
|
||||||
settings: (
|
settings: (
|
||||||
<PaneSettings login={this.props.login} update={this.props.update} />
|
<PaneSettings login={this.props.login} update={this.props.update} />
|
||||||
),
|
),
|
||||||
|
@ -72,6 +160,17 @@ export class Panes extends React.Component<Props, State, {}> {
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.props.userRole === "admin") {
|
||||||
|
panesMap = panesMap.set(
|
||||||
|
"admin",
|
||||||
|
<AdminPane
|
||||||
|
users={this.props.admin.users}
|
||||||
|
roles={this.props.admin.roles}
|
||||||
|
update={this.props.update}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const panes = panesMap.keySeq().map((paneName: string): JSX.Element => {
|
const panes = panesMap.keySeq().map((paneName: string): JSX.Element => {
|
||||||
const isDisplay = displaying === paneName ? "" : "hidden";
|
const isDisplay = displaying === paneName ? "" : "hidden";
|
||||||
return (
|
return (
|
||||||
|
@ -84,24 +183,25 @@ export class Panes extends React.Component<Props, State, {}> {
|
||||||
const btnClass = displaying === "login" ? "hidden" : "";
|
const btnClass = displaying === "login" ? "hidden" : "";
|
||||||
return (
|
return (
|
||||||
<div id="panes" className={displaying === "" ? "hidden" : ""}>
|
<div id="panes" className={displaying === "" ? "hidden" : ""}>
|
||||||
|
<div className="root-container">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="flex-list-container padding-l">
|
<div className="flex-list-container padding-l">
|
||||||
<h3 className="flex-list-item-l txt-cap">{displaying}</h3>
|
<h3 className="flex-list-item-l txt-cap">{displaying}</h3>
|
||||||
<div className="flex-list-item-r">
|
<div className="flex-list-item-r">
|
||||||
<button
|
<button
|
||||||
onClick={this.closePane}
|
onClick={this.closePane}
|
||||||
className={`black0-bg white-font ${btnClass}`}
|
className={`red0-bg white-font ${btnClass}`}
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hr white0-bg margin-b-m margin-l-m margin-r-m"></div>
|
|
||||||
{panes}
|
|
||||||
|
|
||||||
<div className="padding-l"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{panes}
|
||||||
|
</div>
|
||||||
|
{/* <div className="hr white0-bg margin-b-m margin-l-m margin-r-m"></div> */}
|
||||||
|
{/* <div className="padding-l"></div> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ export interface Props {
|
||||||
browser: BrowserProps;
|
browser: BrowserProps;
|
||||||
authPane: PaneLoginProps;
|
authPane: PaneLoginProps;
|
||||||
panes: PanesProps;
|
panes: PanesProps;
|
||||||
admin: PaneAdminProps;
|
|
||||||
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,15 +37,22 @@ export class RootFrame extends React.Component<Props, State, {}> {
|
||||||
this.props.update(PanesUpdater.updateState);
|
this.props.update(PanesUpdater.updateState);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
showAdmin = () => {
|
||||||
|
PanesUpdater.displayPane("admin");
|
||||||
|
this.props.update(PanesUpdater.updateState);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const update = this.props.update;
|
const update = this.props.update;
|
||||||
return (
|
return (
|
||||||
<div className="theme-white desktop">
|
<div className="theme-white desktop">
|
||||||
<div id="bg" className="bg bg-img font-m">
|
<div id="bg" className="bg bg-img font-m">
|
||||||
<Panes
|
<Panes
|
||||||
|
userRole={this.props.panes.userRole}
|
||||||
displaying={this.props.panes.displaying}
|
displaying={this.props.panes.displaying}
|
||||||
paneNames={this.props.panes.paneNames}
|
paneNames={this.props.panes.paneNames}
|
||||||
login={this.props.authPane}
|
login={this.props.authPane}
|
||||||
|
admin={this.props.panes.admin}
|
||||||
update={update}
|
update={update}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -68,6 +74,12 @@ export class RootFrame extends React.Component<Props, State, {}> {
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={this.showAdmin}
|
||||||
|
className="grey1-bg white-font margin-r-m"
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { updater as BrowserUpdater } from "./browser.updater";
|
import { updater as BrowserUpdater } from "./browser.updater";
|
||||||
|
import { Updater as PanesUpdater } from "./panes";
|
||||||
import { ICoreState, init } from "./core_state";
|
import { ICoreState, init } from "./core_state";
|
||||||
import { RootFrame } from "./root_frame";
|
import { RootFrame } from "./root_frame";
|
||||||
import { FilesClient } from "../client/files";
|
import { FilesClient } from "../client/files";
|
||||||
|
@ -26,6 +27,19 @@ export class StateMgr extends React.Component<Props, State, {}> {
|
||||||
})
|
})
|
||||||
.then((_: boolean) => {
|
.then((_: boolean) => {
|
||||||
this.update(BrowserUpdater().setBrowser);
|
this.update(BrowserUpdater().setBrowser);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return PanesUpdater.self();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return PanesUpdater.listRoles();
|
||||||
|
})
|
||||||
|
.then((_: boolean) => {
|
||||||
|
return PanesUpdater.listUsers();
|
||||||
|
})
|
||||||
|
.then((_: boolean) => {
|
||||||
|
console.log(PanesUpdater);
|
||||||
|
this.update(PanesUpdater.updateState);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,7 +55,6 @@ export class StateMgr extends React.Component<Props, State, {}> {
|
||||||
update={this.update}
|
update={this.update}
|
||||||
browser={this.state.panel.browser}
|
browser={this.state.panel.browser}
|
||||||
panes={this.state.panel.panes}
|
panes={this.state.panel.panes}
|
||||||
admin={this.state.panel.admin}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,7 +118,7 @@ func (h *FileHandlers) Create(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpFilePath := q.GetTmpPath(userID, req.Path)
|
tmpFilePath := q.UploadPath(userID, req.Path)
|
||||||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||||||
locker.Exec(func() {
|
locker.Exec(func() {
|
||||||
err := h.deps.FS().Create(tmpFilePath)
|
err := h.deps.FS().Create(tmpFilePath)
|
||||||
|
@ -295,7 +295,7 @@ func (h *FileHandlers) UploadChunk(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpFilePath := q.GetTmpPath(userID, req.Path)
|
tmpFilePath := q.UploadPath(userID, req.Path)
|
||||||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||||||
locker.Exec(func() {
|
locker.Exec(func() {
|
||||||
var err error
|
var err error
|
||||||
|
@ -407,7 +407,7 @@ func (h *FileHandlers) UploadStatus(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpFilePath := q.GetTmpPath(userID, filePath)
|
tmpFilePath := q.UploadPath(userID, filePath)
|
||||||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||||||
locker.Exec(func() {
|
locker.Exec(func() {
|
||||||
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(userID, tmpFilePath)
|
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(userID, tmpFilePath)
|
||||||
|
@ -549,7 +549,8 @@ func (h *FileHandlers) List(c *gin.Context) {
|
||||||
|
|
||||||
func (h *FileHandlers) ListHome(c *gin.Context) {
|
func (h *FileHandlers) ListHome(c *gin.Context) {
|
||||||
userID := c.MustGet(q.UserIDParam).(string)
|
userID := c.MustGet(q.UserIDParam).(string)
|
||||||
infos, err := h.deps.FS().ListDir(userID)
|
fsPath := q.FsRootPath(userID, "/")
|
||||||
|
infos, err := h.deps.FS().ListDir(fsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(q.ErrResp(c, 500, err))
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
return
|
return
|
||||||
|
@ -565,7 +566,7 @@ func (h *FileHandlers) ListHome(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, &ListResp{
|
c.JSON(200, &ListResp{
|
||||||
Cwd: userID,
|
Cwd: fsPath,
|
||||||
Metadatas: metadatas,
|
Metadatas: metadatas,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -606,7 +607,7 @@ func (h *FileHandlers) DelUploading(c *gin.Context) {
|
||||||
userID := c.MustGet(q.UserIDParam).(string)
|
userID := c.MustGet(q.UserIDParam).(string)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
tmpFilePath := q.GetTmpPath(userID, filePath)
|
tmpFilePath := q.UploadPath(userID, filePath)
|
||||||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||||||
locker.Exec(func() {
|
locker.Exec(func() {
|
||||||
err = h.deps.FS().Remove(tmpFilePath)
|
err = h.deps.FS().Remove(tmpFilePath)
|
||||||
|
|
|
@ -102,7 +102,12 @@ func (um *UploadMgr) DelInfo(user, filePath string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (um *UploadMgr) ListInfo(user string) ([]*UploadInfo, error) {
|
func (um *UploadMgr) ListInfo(user string) ([]*UploadInfo, error) {
|
||||||
infoMap, err := um.kv.ListStringsIn(UploadNS(user))
|
ns := UploadNS(user)
|
||||||
|
if !um.kv.HasNamespace(ns) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
infoMap, err := um.kv.ListStringsIn(ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,10 +39,14 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error)
|
||||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/logout"): true,
|
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/logout"): true,
|
||||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/users/isauthed"): true,
|
apiRuleCname(userstore.AdminRole, "GET", "/v1/users/isauthed"): true,
|
||||||
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/users/pwd"): true,
|
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/users/pwd"): true,
|
||||||
|
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/users/pwd/force-set"): true,
|
||||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/"): true,
|
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/"): true,
|
||||||
|
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/users/"): true,
|
||||||
|
apiRuleCname(userstore.AdminRole, "GET", "/v1/users/list"): true,
|
||||||
|
apiRuleCname(userstore.AdminRole, "GET", "/v1/users/self"): true,
|
||||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/roles/"): true,
|
apiRuleCname(userstore.AdminRole, "POST", "/v1/roles/"): true,
|
||||||
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/roles/"): true,
|
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/roles/"): true,
|
||||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/roles/"): true,
|
apiRuleCname(userstore.AdminRole, "GET", "/v1/roles/list"): true,
|
||||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/files"): true,
|
apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/files"): true,
|
||||||
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/files"): true,
|
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/files"): true,
|
||||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files"): true,
|
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files"): true,
|
||||||
|
@ -63,6 +67,7 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error)
|
||||||
apiRuleCname(userstore.UserRole, "POST", "/v1/users/logout"): true,
|
apiRuleCname(userstore.UserRole, "POST", "/v1/users/logout"): true,
|
||||||
apiRuleCname(userstore.UserRole, "GET", "/v1/users/isauthed"): true,
|
apiRuleCname(userstore.UserRole, "GET", "/v1/users/isauthed"): true,
|
||||||
apiRuleCname(userstore.UserRole, "PATCH", "/v1/users/pwd"): true,
|
apiRuleCname(userstore.UserRole, "PATCH", "/v1/users/pwd"): true,
|
||||||
|
apiRuleCname(userstore.UserRole, "GET", "/v1/users/self"): true,
|
||||||
apiRuleCname(userstore.UserRole, "POST", "/v1/fs/files"): true,
|
apiRuleCname(userstore.UserRole, "POST", "/v1/fs/files"): true,
|
||||||
apiRuleCname(userstore.UserRole, "DELETE", "/v1/fs/files"): true,
|
apiRuleCname(userstore.UserRole, "DELETE", "/v1/fs/files"): true,
|
||||||
apiRuleCname(userstore.UserRole, "GET", "/v1/fs/files"): true,
|
apiRuleCname(userstore.UserRole, "GET", "/v1/fs/files"): true,
|
||||||
|
@ -82,6 +87,7 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error)
|
||||||
apiRuleCname(userstore.VisitorRole, "GET", publicPath): true,
|
apiRuleCname(userstore.VisitorRole, "GET", publicPath): true,
|
||||||
apiRuleCname(userstore.VisitorRole, "POST", "/v1/users/login"): true,
|
apiRuleCname(userstore.VisitorRole, "POST", "/v1/users/login"): true,
|
||||||
apiRuleCname(userstore.VisitorRole, "GET", "/v1/users/isauthed"): true,
|
apiRuleCname(userstore.VisitorRole, "GET", "/v1/users/isauthed"): true,
|
||||||
|
apiRuleCname(userstore.VisitorRole, "GET", "/v1/users/self"): true,
|
||||||
apiRuleCname(userstore.VisitorRole, "GET", "/v1/fs/files"): true,
|
apiRuleCname(userstore.VisitorRole, "GET", "/v1/fs/files"): true,
|
||||||
apiRuleCname(userstore.VisitorRole, "OPTIONS", "/v1/settings/health"): true,
|
apiRuleCname(userstore.VisitorRole, "OPTIONS", "/v1/settings/health"): true,
|
||||||
}
|
}
|
||||||
|
@ -97,12 +103,12 @@ func (h *MultiUsersSvc) Init(adminName, adminPwd string) (string, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
userID := "0"
|
userID := "0"
|
||||||
fsPath := q.HomePath(userID, "/")
|
fsPath := q.FsRootPath(userID, "/")
|
||||||
if err = h.deps.FS().MkdirAll(fsPath); err != nil {
|
if err = h.deps.FS().MkdirAll(fsPath); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
uploadingsPath := q.GetTmpPath(userID, "/")
|
uploadFolder := q.UploadFolder(userID)
|
||||||
if err = h.deps.FS().MkdirAll(uploadingsPath); err != nil {
|
if err = h.deps.FS().MkdirAll(uploadFolder); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,6 +237,57 @@ func (h *MultiUsersSvc) SetPwd(c *gin.Context) {
|
||||||
c.JSON(q.Resp(200))
|
c.JSON(q.Resp(200))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ForceSetPwdReq struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
NewPwd string `json:"newPwd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MultiUsersSvc) ForceSetPwd(c *gin.Context) {
|
||||||
|
req := &ForceSetPwdReq{}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(q.ErrResp(c, 400, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := h.getUserInfo(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(q.ErrResp(c, 401, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if claims[q.RoleParam] != userstore.AdminRole {
|
||||||
|
c.JSON(q.ErrResp(c, 403, errors.New("operation denied")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetUID, err := strconv.ParseUint(req.ID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetUser, err := h.deps.Users().GetUser(targetUID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if targetUser.Role == userstore.AdminRole {
|
||||||
|
c.JSON(q.ErrResp(c, 403, errors.New("can not set admin's password")))
|
||||||
|
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(targetUser.ID, string(newHash))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(q.Resp(200))
|
||||||
|
}
|
||||||
|
|
||||||
type AddUserReq struct {
|
type AddUserReq struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Pwd string `json:"pwd"`
|
Pwd string `json:"pwd"`
|
||||||
|
@ -267,14 +324,14 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) {
|
||||||
|
|
||||||
// TODO: following operations must be atomic
|
// TODO: following operations must be atomic
|
||||||
// TODO: check if the folders already exists
|
// TODO: check if the folders already exists
|
||||||
userID := c.MustGet(q.UserIDParam).(string)
|
uidStr := fmt.Sprint(uid)
|
||||||
homePath := q.HomePath(userID, "/")
|
fsRootFolder := q.FsRootPath(uidStr, "/")
|
||||||
if err = h.deps.FS().MkdirAll(homePath); err != nil {
|
if err = h.deps.FS().MkdirAll(fsRootFolder); err != nil {
|
||||||
c.JSON(q.ErrResp(c, 500, err))
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uploadingsPath := q.GetTmpPath(userID, "/")
|
uploadFolder := q.UploadFolder(uidStr)
|
||||||
if err = h.deps.FS().MkdirAll(uploadingsPath); err != nil {
|
if err = h.deps.FS().MkdirAll(uploadFolder); err != nil {
|
||||||
c.JSON(q.ErrResp(c, 500, err))
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -293,6 +350,71 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) {
|
||||||
c.JSON(200, &AddUserResp{ID: fmt.Sprint(uid)})
|
c.JSON(200, &AddUserResp{ID: fmt.Sprint(uid)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DelUserResp struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MultiUsersSvc) DelUser(c *gin.Context) {
|
||||||
|
userIDStr := c.Query(q.UserIDParam)
|
||||||
|
userID, err := strconv.ParseUint(userIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(q.ErrResp(c, 400, fmt.Errorf("invalid users ID %w", err)))
|
||||||
|
return
|
||||||
|
} else if userID == 0 {
|
||||||
|
c.JSON(q.ErrResp(c, 400, errors.New("It is not allowed to delete root")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := h.getUserInfo(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(q.ErrResp(c, 401, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if claims[q.UserIDParam] == userIDStr {
|
||||||
|
c.JSON(q.ErrResp(c, 403, errors.New("can not delete self")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: try to make following atomic
|
||||||
|
err = h.deps.Users().DelUser(userID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move the folder to recycle bin when it failed to remove it
|
||||||
|
homePath := userIDStr
|
||||||
|
if err = h.deps.FS().Remove(homePath); err != nil {
|
||||||
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, &DelUserResp{ID: userIDStr})
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListUsersResp struct {
|
||||||
|
Users []*userstore.User `json:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MultiUsersSvc) ListUsers(c *gin.Context) {
|
||||||
|
// TODO: pagination is not enabled
|
||||||
|
// lastID := 0
|
||||||
|
// lastIDStr := c.Query(q.LastID)
|
||||||
|
// if lastIDStr != "" {
|
||||||
|
// lastID, err := strconv.Atoi(lastIDStr)
|
||||||
|
// if err != nil {
|
||||||
|
// c.JSON(q.ErrResp(c, 400, fmt.Errorf("invalid param %w", err)))
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
users, err := h.deps.Users().ListUsers()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(q.ErrResp(c, 500, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, &ListUsersResp{Users: users})
|
||||||
|
}
|
||||||
|
|
||||||
type AddRoleReq struct {
|
type AddRoleReq struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
}
|
}
|
||||||
|
@ -405,3 +527,23 @@ func (h *MultiUsersSvc) isValidRole(role string) error {
|
||||||
}
|
}
|
||||||
return h.isValidUserName(role)
|
return h.isValidUserName(role)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelfResp struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MultiUsersSvc) Self(c *gin.Context) {
|
||||||
|
claims, err := h.getUserInfo(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(q.ErrResp(c, 401, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, &SelfResp{
|
||||||
|
ID: claims[q.UserIDParam],
|
||||||
|
Name: claims[q.UserParam],
|
||||||
|
Role: claims[q.RoleParam],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ var (
|
||||||
// dirs
|
// dirs
|
||||||
UploadDir = "uploadings"
|
UploadDir = "uploadings"
|
||||||
FsDir = "files"
|
FsDir = "files"
|
||||||
|
FsRootDir = "files"
|
||||||
|
|
||||||
UserIDParam = "uid"
|
UserIDParam = "uid"
|
||||||
UserParam = "user"
|
UserParam = "user"
|
||||||
|
@ -21,6 +22,7 @@ var (
|
||||||
RoleParam = "role"
|
RoleParam = "role"
|
||||||
ExpireParam = "expire"
|
ExpireParam = "expire"
|
||||||
TokenCookie = "tk"
|
TokenCookie = "tk"
|
||||||
|
LastID = "lid"
|
||||||
|
|
||||||
ErrAccessDenied = errors.New("access denied")
|
ErrAccessDenied = errors.New("access denied")
|
||||||
ErrUnauthorized = errors.New("unauthorized")
|
ErrUnauthorized = errors.New("unauthorized")
|
||||||
|
@ -131,6 +133,18 @@ func HomePath(userID, relFilePath string) string {
|
||||||
return filepath.Join(userID, relFilePath)
|
return filepath.Join(userID, relFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FsRootPath(userID, relFilePath string) string {
|
||||||
|
return filepath.Join(userID, FsRootDir, relFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
func GetTmpPath(userID, relFilePath string) string {
|
func GetTmpPath(userID, relFilePath string) string {
|
||||||
return filepath.Join(UploadDir, userID, fmt.Sprintf("%x", sha1.Sum([]byte(relFilePath))))
|
return filepath.Join(UploadDir, userID, fmt.Sprintf("%x", sha1.Sum([]byte(relFilePath))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UploadPath(userID, relFilePath string) string {
|
||||||
|
return filepath.Join(UploadFolder(userID), fmt.Sprintf("%x", sha1.Sum([]byte(relFilePath))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadFolder(userID string) string {
|
||||||
|
return filepath.Join(userID, UploadDir)
|
||||||
|
}
|
||||||
|
|
|
@ -72,6 +72,18 @@ func (bp *BoltPvd) DelNamespace(nsName string) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bp *BoltPvd) HasNamespace(nsName string) bool {
|
||||||
|
err := bp.db.View(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte(nsName))
|
||||||
|
if b == nil {
|
||||||
|
return ErrBucketNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func (bp *BoltPvd) Close() error {
|
func (bp *BoltPvd) Close() error {
|
||||||
return bp.db.Close()
|
return bp.db.Close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ var ErrNoLock = errors.New("no lock to unlock")
|
||||||
type IKVStore interface {
|
type IKVStore interface {
|
||||||
AddNamespace(nsName string) error
|
AddNamespace(nsName string) error
|
||||||
DelNamespace(nsName string) error
|
DelNamespace(nsName string) error
|
||||||
|
HasNamespace(nsName string) bool
|
||||||
GetBool(key string) (bool, bool)
|
GetBool(key string) (bool, bool)
|
||||||
GetBoolIn(ns, key string) (bool, bool)
|
GetBoolIn(ns, key string) (bool, bool)
|
||||||
SetBool(key string, val bool) error
|
SetBool(key string, val bool) error
|
||||||
|
|
|
@ -178,12 +178,16 @@ 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.PATCH("/pwd/force-set", userHdrs.ForceSetPwd)
|
||||||
usersAPI.POST("/", userHdrs.AddUser)
|
usersAPI.POST("/", userHdrs.AddUser)
|
||||||
|
usersAPI.DELETE("/", userHdrs.DelUser)
|
||||||
|
usersAPI.GET("/list", userHdrs.ListUsers)
|
||||||
|
usersAPI.GET("/self", userHdrs.Self)
|
||||||
|
|
||||||
rolesAPI := v1.Group("/roles")
|
rolesAPI := v1.Group("/roles")
|
||||||
rolesAPI.POST("/", userHdrs.AddRole)
|
rolesAPI.POST("/", userHdrs.AddRole)
|
||||||
rolesAPI.DELETE("/", userHdrs.DelRole)
|
rolesAPI.DELETE("/", userHdrs.DelRole)
|
||||||
rolesAPI.GET("/", userHdrs.ListRoles)
|
rolesAPI.GET("/list", userHdrs.ListRoles)
|
||||||
|
|
||||||
filesAPI := v1.Group("/fs")
|
filesAPI := v1.Group("/fs")
|
||||||
filesAPI.POST("/files", fileHdrs.Create)
|
filesAPI.POST("/files", fileHdrs.Create)
|
||||||
|
|
134
src/server/server_concurrency_test.go
Normal file
134
src/server/server_concurrency_test.go
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ihexxa/quickshare/src/client"
|
||||||
|
q "github.com/ihexxa/quickshare/src/handlers"
|
||||||
|
"github.com/ihexxa/quickshare/src/userstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConcurrency(t *testing.T) {
|
||||||
|
addr := "http://127.0.0.1:8686"
|
||||||
|
root := "testData"
|
||||||
|
config := `{
|
||||||
|
"users": {
|
||||||
|
"enableAuth": true,
|
||||||
|
"minUserNameLen": 2,
|
||||||
|
"minPwdLen": 4
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"debug": true
|
||||||
|
},
|
||||||
|
"fs": {
|
||||||
|
"root": "testData"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
adminName := "qs"
|
||||||
|
adminPwd := "quicksh@re"
|
||||||
|
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()
|
||||||
|
// fs := srv.depsFS()
|
||||||
|
if !waitForReady(addr) {
|
||||||
|
t.Fatal("fail to start server")
|
||||||
|
}
|
||||||
|
|
||||||
|
usersCl := client.NewSingleUserClient(addr)
|
||||||
|
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(), q.TokenCookie)
|
||||||
|
|
||||||
|
userCount := 5
|
||||||
|
userPwd := "1234"
|
||||||
|
users := map[string]string{}
|
||||||
|
getUserName := func(id int) string {
|
||||||
|
return fmt.Sprintf("user_%d", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range make([]int, userCount) {
|
||||||
|
userName := getUserName(i)
|
||||||
|
|
||||||
|
resp, adResp, errs := usersCl.AddUser(userName, userPwd, userstore.UserRole, token)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatal(errs)
|
||||||
|
} else if resp.StatusCode != 200 {
|
||||||
|
t.Fatal("failed to add user")
|
||||||
|
}
|
||||||
|
|
||||||
|
users[userName] = adResp.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
filesSize := 10
|
||||||
|
mockClient := func(id, name, pwd string, wg *sync.WaitGroup) {
|
||||||
|
usersCl := client.NewSingleUserClient(addr)
|
||||||
|
resp, _, errs := usersCl.Login(name, pwd)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatal(errs)
|
||||||
|
} else if resp.StatusCode != 200 {
|
||||||
|
t.Fatal("failed to add user")
|
||||||
|
}
|
||||||
|
token := client.GetCookie(resp.Cookies(), q.TokenCookie)
|
||||||
|
|
||||||
|
files := map[string]string{}
|
||||||
|
content := "12345678"
|
||||||
|
for i := range make([]int, filesSize, filesSize) {
|
||||||
|
files[fmt.Sprintf("%s/files/home_file_%d", id, i)] = content
|
||||||
|
}
|
||||||
|
|
||||||
|
for filePath, content := range files {
|
||||||
|
assertUploadOK(t, filePath, content, addr, token)
|
||||||
|
assertDownloadOK(t, filePath, content, addr, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
filesCl := client.NewFilesClient(addr, token)
|
||||||
|
resp, lsResp, errs := filesCl.ListHome()
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatal(errs)
|
||||||
|
} else if resp.StatusCode != 200 {
|
||||||
|
t.Fatal("failed to add user")
|
||||||
|
}
|
||||||
|
|
||||||
|
if lsResp.Cwd != fmt.Sprintf("%s/files", id) {
|
||||||
|
t.Fatalf("incorrct cwd (%s)", lsResp.Cwd)
|
||||||
|
} else if len(lsResp.Metadatas) != len(files) {
|
||||||
|
t.Fatalf("incorrct metadata size (%d)", len(lsResp.Metadatas))
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
t.Run("ListHome", func(t *testing.T) {
|
||||||
|
for userName, userID := range users {
|
||||||
|
wg.Add(1)
|
||||||
|
go mockClient(userID, userName, userPwd, &wg)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
})
|
||||||
|
|
||||||
|
resp, _, errs = usersCl.Logout(token)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatal(errs)
|
||||||
|
} else if resp.StatusCode != 200 {
|
||||||
|
t.Fatal(resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,11 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -65,120 +61,67 @@ func TestFileHandlers(t *testing.T) {
|
||||||
token := client.GetCookie(resp.Cookies(), q.TokenCookie)
|
token := client.GetCookie(resp.Cookies(), q.TokenCookie)
|
||||||
cl := client.NewFilesClient(addr, token)
|
cl := client.NewFilesClient(addr, token)
|
||||||
|
|
||||||
assertUploadOK := func(t *testing.T, filePath, content string) bool {
|
// TODO: remove all files under home folder before testing
|
||||||
cl := client.NewFilesClient(addr, token)
|
// or the count of files is incorrect
|
||||||
|
t.Run("ListHome", func(t *testing.T) {
|
||||||
|
files := map[string]string{
|
||||||
|
"0/files/home_file1": "12345678",
|
||||||
|
"0/files/home_file2": "12345678",
|
||||||
|
}
|
||||||
|
|
||||||
fileSize := int64(len([]byte(content)))
|
for filePath, content := range files {
|
||||||
res, _, errs := cl.Create(filePath, fileSize)
|
assertUploadOK(t, filePath, content, addr, token)
|
||||||
|
|
||||||
|
err = fs.Sync()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, lhResp, errs := cl.ListHome()
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Error(errs)
|
t.Fatal(errs)
|
||||||
return false
|
} else if resp.StatusCode != 200 {
|
||||||
} else if res.StatusCode != 200 {
|
t.Fatal(resp.StatusCode)
|
||||||
t.Error(res.StatusCode)
|
} else if lhResp.Cwd != "0/files" {
|
||||||
return false
|
t.Fatalf("incorrect ListHome cwd %s", lhResp.Cwd)
|
||||||
|
} else if len(lhResp.Metadatas) != len(files) {
|
||||||
|
for _, metadata := range lhResp.Metadatas {
|
||||||
|
fmt.Printf("%v\n", metadata)
|
||||||
|
}
|
||||||
|
t.Fatalf("incorrect ListHome content %d", len(lhResp.Metadatas))
|
||||||
}
|
}
|
||||||
|
|
||||||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
infos := map[string]*fileshdr.MetadataResp{}
|
||||||
res, _, errs = cl.UploadChunk(filePath, base64Content, 0)
|
for _, metadata := range lhResp.Metadatas {
|
||||||
if len(errs) > 0 {
|
infos[metadata.Name] = metadata
|
||||||
t.Error(errs)
|
|
||||||
return false
|
|
||||||
} else if res.StatusCode != 200 {
|
|
||||||
t.Error(res.StatusCode)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
if infos["home_file1"].Size != int64(len(files["0/files/home_file1"])) {
|
||||||
|
t.Fatalf("incorrect file size %d", infos["home_file1"].Size)
|
||||||
|
} else if infos["home_file1"].IsDir {
|
||||||
|
t.Fatal("incorrect item type")
|
||||||
|
}
|
||||||
|
if infos["home_file2"].Size != int64(len(files["0/files/home_file2"])) {
|
||||||
|
t.Fatalf("incorrect file size %d", infos["home_file2"].Size)
|
||||||
|
} else if infos["home_file2"].IsDir {
|
||||||
|
t.Fatal("incorrect item type")
|
||||||
}
|
}
|
||||||
|
|
||||||
assetDownloadOK := func(t *testing.T, filePath, content string) bool {
|
|
||||||
var (
|
|
||||||
res *http.Response
|
|
||||||
body string
|
|
||||||
errs []error
|
|
||||||
fileSize = int64(len([]byte(content)))
|
|
||||||
)
|
|
||||||
|
|
||||||
// cl := client.NewFilesClient(addr)
|
|
||||||
|
|
||||||
rd := rand.Intn(3)
|
|
||||||
switch rd {
|
|
||||||
case 0:
|
|
||||||
res, body, errs = cl.Download(filePath, map[string]string{})
|
|
||||||
case 1:
|
|
||||||
res, body, errs = cl.Download(filePath, map[string]string{
|
|
||||||
"Range": fmt.Sprintf("bytes=0-%d", fileSize-1),
|
|
||||||
})
|
})
|
||||||
case 2:
|
|
||||||
res, body, errs = cl.Download(filePath, map[string]string{
|
|
||||||
"Range": fmt.Sprintf("bytes=0-%d, %d-%d", (fileSize-1)/2, (fileSize-1)/2+1, fileSize-1),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fileName := path.Base(filePath)
|
|
||||||
contentDispositionHeader := res.Header.Get("Content-Disposition")
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Error(errs)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if res.StatusCode != 200 && res.StatusCode != 206 {
|
|
||||||
t.Error(res.StatusCode)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if contentDispositionHeader != fmt.Sprintf(`attachment; filename="%s"`, fileName) {
|
|
||||||
t.Errorf("incorrect Content-Disposition header: %s", contentDispositionHeader)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch rd {
|
|
||||||
case 0:
|
|
||||||
if body != content {
|
|
||||||
t.Errorf("body not equal got(%s) expect(%s)\n", body, content)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case 1:
|
|
||||||
if body[2:] != content { // body returned by gorequest contains the first CRLF
|
|
||||||
t.Errorf("body not equal got(%s) expect(%s)\n", body[2:], content)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
body = body[2:] // body returned by gorequest contains the first CRLF
|
|
||||||
realBody := ""
|
|
||||||
boundaryEnd := strings.Index(body, "\r\n")
|
|
||||||
boundary := body[0:boundaryEnd]
|
|
||||||
bodyParts := strings.Split(body, boundary)
|
|
||||||
|
|
||||||
for i, bodyPart := range bodyParts {
|
|
||||||
if i == 0 || i == len(bodyParts)-1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
start := strings.Index(bodyPart, "\r\n\r\n")
|
|
||||||
|
|
||||||
fmt.Printf("<%s>", bodyPart[start+4:len(bodyPart)-2]) // ignore the last CRLF
|
|
||||||
realBody += bodyPart[start+4 : len(bodyPart)-2]
|
|
||||||
}
|
|
||||||
if realBody != content {
|
|
||||||
t.Errorf("multi body not equal got(%s) expect(%s)\n", realBody, content)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("test uploading files with duplicated names", func(t *testing.T) {
|
t.Run("test uploading files with duplicated names", func(t *testing.T) {
|
||||||
files := map[string]string{
|
files := map[string]string{
|
||||||
"0/dupdir/dup_file1": "12345678",
|
"0/files/dupdir/dup_file1": "12345678",
|
||||||
"0/dupdir/dup_file2.ext": "12345678",
|
"0/files/dupdir/dup_file2.ext": "12345678",
|
||||||
}
|
}
|
||||||
renames := map[string]string{
|
renames := map[string]string{
|
||||||
"0/dupdir/dup_file1": "0/dupdir/dup_file1_1",
|
"0/files/dupdir/dup_file1": "0/files/dupdir/dup_file1_1",
|
||||||
"0/dupdir/dup_file2.ext": "0/dupdir/dup_file2_1.ext",
|
"0/files/dupdir/dup_file2.ext": "0/files/dupdir/dup_file2_1.ext",
|
||||||
}
|
}
|
||||||
|
|
||||||
for filePath, content := range files {
|
for filePath, content := range files {
|
||||||
for i := 0; i < 2; i++ {
|
for i := 0; i < 2; i++ {
|
||||||
assertUploadOK(t, filePath, content)
|
assertUploadOK(t, filePath, content, addr, token)
|
||||||
|
|
||||||
err = fs.Sync()
|
err = fs.Sync()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -186,13 +129,13 @@ func TestFileHandlers(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
assetDownloadOK(t, filePath, content)
|
assertDownloadOK(t, filePath, content, addr, token)
|
||||||
} else if i == 1 {
|
} else if i == 1 {
|
||||||
renamedFilePath, ok := renames[filePath]
|
renamedFilePath, ok := renames[filePath]
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("new name not found")
|
t.Fatal("new name not found")
|
||||||
}
|
}
|
||||||
assetDownloadOK(t, renamedFilePath, content)
|
assertDownloadOK(t, renamedFilePath, content, addr, token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,8 +143,8 @@ func TestFileHandlers(t *testing.T) {
|
||||||
|
|
||||||
t.Run("test files APIs: Create-UploadChunk-UploadStatus-Metadata-Delete", func(t *testing.T) {
|
t.Run("test files APIs: Create-UploadChunk-UploadStatus-Metadata-Delete", func(t *testing.T) {
|
||||||
for filePath, content := range map[string]string{
|
for filePath, content := range map[string]string{
|
||||||
"0/path1/f1.md": "1111 1111 1111 1111",
|
"0/files/path1/f1.md": "1111 1111 1111 1111",
|
||||||
"0/path1/path2/f2.md": "1010 1010 1111 0000 0010",
|
"0/files/path1/path2/f2.md": "1010 1010 1111 0000 0010",
|
||||||
} {
|
} {
|
||||||
fileSize := int64(len([]byte(content)))
|
fileSize := int64(len([]byte(content)))
|
||||||
// create a file
|
// create a file
|
||||||
|
@ -213,7 +156,7 @@ func TestFileHandlers(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check uploading file
|
// check uploading file
|
||||||
uploadFilePath := path.Join(q.UploadDir, "0", fmt.Sprintf("%x", sha1.Sum([]byte(filePath))))
|
uploadFilePath := q.UploadPath("0", filePath)
|
||||||
info, err := fs.Stat(uploadFilePath)
|
info, err := fs.Stat(uploadFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -290,11 +233,11 @@ func TestFileHandlers(t *testing.T) {
|
||||||
|
|
||||||
t.Run("test dirs APIs: Mkdir-Create-UploadChunk-List", func(t *testing.T) {
|
t.Run("test dirs APIs: Mkdir-Create-UploadChunk-List", func(t *testing.T) {
|
||||||
for dirPath, files := range map[string]map[string]string{
|
for dirPath, files := range map[string]map[string]string{
|
||||||
"0/dir/path1": map[string]string{
|
"0/files/dir/path1": map[string]string{
|
||||||
"f1.md": "11111",
|
"f1.md": "11111",
|
||||||
"f2.md": "22222222222",
|
"f2.md": "22222222222",
|
||||||
},
|
},
|
||||||
"0/dir/path2/path2": map[string]string{
|
"0/files/dir/path2/path2": map[string]string{
|
||||||
"f3.md": "3333333",
|
"f3.md": "3333333",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
@ -307,7 +250,7 @@ func TestFileHandlers(t *testing.T) {
|
||||||
|
|
||||||
for fileName, content := range files {
|
for fileName, content := range files {
|
||||||
filePath := filepath.Join(dirPath, fileName)
|
filePath := filepath.Join(dirPath, fileName)
|
||||||
assertUploadOK(t, filePath, content)
|
assertUploadOK(t, filePath, content, addr, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = fs.Sync()
|
err = fs.Sync()
|
||||||
|
@ -331,8 +274,8 @@ func TestFileHandlers(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test operation APIs: Mkdir-Create-UploadChunk-Move-List", func(t *testing.T) {
|
t.Run("test operation APIs: Mkdir-Create-UploadChunk-Move-List", func(t *testing.T) {
|
||||||
srcDir := "0/move/src"
|
srcDir := "0/files/move/src"
|
||||||
dstDir := "0/move/dst"
|
dstDir := "0/files/move/dst"
|
||||||
|
|
||||||
for _, dirPath := range []string{srcDir, dstDir} {
|
for _, dirPath := range []string{srcDir, dstDir} {
|
||||||
res, _, errs := cl.Mkdir(dirPath)
|
res, _, errs := cl.Mkdir(dirPath)
|
||||||
|
@ -352,7 +295,7 @@ func TestFileHandlers(t *testing.T) {
|
||||||
oldPath := filepath.Join(srcDir, fileName)
|
oldPath := filepath.Join(srcDir, fileName)
|
||||||
newPath := filepath.Join(dstDir, fileName)
|
newPath := filepath.Join(dstDir, fileName)
|
||||||
// fileSize := int64(len([]byte(content)))
|
// fileSize := int64(len([]byte(content)))
|
||||||
assertUploadOK(t, oldPath, content)
|
assertUploadOK(t, oldPath, content, addr, token)
|
||||||
|
|
||||||
res, _, errs := cl.Move(oldPath, newPath)
|
res, _, errs := cl.Move(oldPath, newPath)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
|
@ -383,17 +326,17 @@ func TestFileHandlers(t *testing.T) {
|
||||||
|
|
||||||
t.Run("test download APIs: Download(normal, ranges)", func(t *testing.T) {
|
t.Run("test download APIs: Download(normal, ranges)", func(t *testing.T) {
|
||||||
for filePath, content := range map[string]string{
|
for filePath, content := range map[string]string{
|
||||||
"0/download/path1/f1": "123456",
|
"0/files/download/path1/f1": "123456",
|
||||||
"0/download/path1/path2": "12345678",
|
"0/files/download/path1/path2": "12345678",
|
||||||
} {
|
} {
|
||||||
assertUploadOK(t, filePath, content)
|
assertUploadOK(t, filePath, content, addr, token)
|
||||||
|
|
||||||
err = fs.Sync()
|
err = fs.Sync()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assetDownloadOK(t, filePath, content)
|
assertDownloadOK(t, filePath, content, addr, token)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -407,7 +350,7 @@ func TestFileHandlers(t *testing.T) {
|
||||||
startClient := func(files []*mockFile) {
|
startClient := func(files []*mockFile) {
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if !assertUploadOK(t, fmt.Sprintf("%s_%d", file.FilePath, i), file.Content) {
|
if !assertUploadOK(t, fmt.Sprintf("%s_%d", file.FilePath, i), file.Content, addr, token) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,7 +359,7 @@ func TestFileHandlers(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !assetDownloadOK(t, fmt.Sprintf("%s_%d", file.FilePath, i), file.Content) {
|
if !assertDownloadOK(t, fmt.Sprintf("%s_%d", file.FilePath, i), file.Content, addr, token) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -446,10 +389,18 @@ func TestFileHandlers(t *testing.T) {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("test uploading APIs: Create, ListUploadings, DelUploading", func(t *testing.T) {
|
t.Run("test uploading APIs: ListUploadings, Create, ListUploadings, DelUploading", func(t *testing.T) {
|
||||||
|
// it should return no error even no file is uploaded
|
||||||
|
res, lResp, errs := cl.ListUploadings()
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatal(errs)
|
||||||
|
} else if res.StatusCode != 200 {
|
||||||
|
t.Fatal(res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
files := map[string]string{
|
files := map[string]string{
|
||||||
"0/uploadings/path1/f1": "123456",
|
"0/files/uploadings/path1/f1": "123456",
|
||||||
"0/uploadings/path1/path2": "12345678",
|
"0/files/uploadings/path1/path2": "12345678",
|
||||||
}
|
}
|
||||||
|
|
||||||
for filePath, content := range files {
|
for filePath, content := range files {
|
||||||
|
@ -462,7 +413,7 @@ func TestFileHandlers(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res, lResp, errs := cl.ListUploadings()
|
res, lResp, errs = cl.ListUploadings()
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
} else if res.StatusCode != 200 {
|
} else if res.StatusCode != 200 {
|
||||||
|
@ -507,7 +458,7 @@ func TestFileHandlers(t *testing.T) {
|
||||||
// cl := client.NewFilesClient(addr)
|
// cl := client.NewFilesClient(addr)
|
||||||
|
|
||||||
files := map[string]string{
|
files := map[string]string{
|
||||||
"0/uploadings/path1/f1": "12345678",
|
"0/files/uploadings/path1/f1": "12345678",
|
||||||
}
|
}
|
||||||
|
|
||||||
for filePath, content := range files {
|
for filePath, content := range files {
|
||||||
|
@ -554,7 +505,7 @@ func TestFileHandlers(t *testing.T) {
|
||||||
t.Fatal("incorrect uploaded size", mRes)
|
t.Fatal("incorrect uploaded size", mRes)
|
||||||
}
|
}
|
||||||
|
|
||||||
assetDownloadOK(t, filePath, content)
|
assertDownloadOK(t, filePath, content, addr, token)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -562,9 +513,9 @@ func TestFileHandlers(t *testing.T) {
|
||||||
// cl := client.NewFilesClient(addr)
|
// cl := client.NewFilesClient(addr)
|
||||||
|
|
||||||
files := map[string]string{
|
files := map[string]string{
|
||||||
"0/uploadings/random/path1/f1": "12345678",
|
"0/files/uploadings/random/path1/f1": "12345678",
|
||||||
"0/uploadings/random/path1/f2": "87654321",
|
"0/files/uploadings/random/path1/f2": "87654321",
|
||||||
"0/uploadings/random/path1/f3": "17654321",
|
"0/files/uploadings/random/path1/f3": "17654321",
|
||||||
}
|
}
|
||||||
|
|
||||||
for filePath, content := range files {
|
for filePath, content := range files {
|
||||||
|
@ -622,7 +573,7 @@ func TestFileHandlers(t *testing.T) {
|
||||||
t.Fatalf("file content not equal: %s", filePath)
|
t.Fatalf("file content not equal: %s", filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
assetDownloadOK(t, filePath, content)
|
assertDownloadOK(t, filePath, content, addr, token)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,16 @@ package server
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ihexxa/quickshare/src/client"
|
"github.com/ihexxa/quickshare/src/client"
|
||||||
|
q "github.com/ihexxa/quickshare/src/handlers"
|
||||||
su "github.com/ihexxa/quickshare/src/handlers/singleuserhdr"
|
su "github.com/ihexxa/quickshare/src/handlers/singleuserhdr"
|
||||||
"github.com/ihexxa/quickshare/src/userstore"
|
"github.com/ihexxa/quickshare/src/userstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSingleUserHandlers(t *testing.T) {
|
func TestUsersHandlers(t *testing.T) {
|
||||||
addr := "http://127.0.0.1:8686"
|
addr := "http://127.0.0.1:8686"
|
||||||
root := "testData"
|
root := "testData"
|
||||||
config := `{
|
config := `{
|
||||||
|
@ -41,6 +43,7 @@ func TestSingleUserHandlers(t *testing.T) {
|
||||||
|
|
||||||
srv := startTestServer(config)
|
srv := startTestServer(config)
|
||||||
defer srv.Shutdown()
|
defer srv.Shutdown()
|
||||||
|
fs := srv.depsFS()
|
||||||
|
|
||||||
usersCl := client.NewSingleUserClient(addr)
|
usersCl := client.NewSingleUserClient(addr)
|
||||||
|
|
||||||
|
@ -48,7 +51,7 @@ func TestSingleUserHandlers(t *testing.T) {
|
||||||
t.Fatal("fail to start server")
|
t.Fatal("fail to start server")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("test users APIs: Login-SetPwd-Logout-Login", func(t *testing.T) {
|
t.Run("test users APIs: Login-Self-SetPwd-Logout-Login", func(t *testing.T) {
|
||||||
resp, _, errs := usersCl.Login(adminName, adminPwd)
|
resp, _, errs := usersCl.Login(adminName, adminPwd)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
|
@ -58,6 +61,17 @@ func TestSingleUserHandlers(t *testing.T) {
|
||||||
|
|
||||||
token := client.GetCookie(resp.Cookies(), su.TokenCookie)
|
token := client.GetCookie(resp.Cookies(), su.TokenCookie)
|
||||||
|
|
||||||
|
resp, selfResp, errs := usersCl.Self(token)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatal(errs)
|
||||||
|
} else if resp.StatusCode != 200 {
|
||||||
|
t.Fatal(resp.StatusCode)
|
||||||
|
} else if selfResp.ID != "0" ||
|
||||||
|
selfResp.Name != adminName ||
|
||||||
|
selfResp.Role != userstore.AdminRole {
|
||||||
|
t.Fatalf("user infos don't match %v", selfResp)
|
||||||
|
}
|
||||||
|
|
||||||
resp, _, errs = usersCl.SetPwd(adminPwd, adminNewPwd, token)
|
resp, _, errs = usersCl.SetPwd(adminPwd, adminNewPwd, token)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
|
@ -90,7 +104,7 @@ func TestSingleUserHandlers(t *testing.T) {
|
||||||
|
|
||||||
token := client.GetCookie(resp.Cookies(), su.TokenCookie)
|
token := client.GetCookie(resp.Cookies(), su.TokenCookie)
|
||||||
|
|
||||||
userName, userPwd := "user", "1234"
|
userName, userPwd := "user_login", "1234"
|
||||||
resp, auResp, errs := usersCl.AddUser(userName, userPwd, userstore.UserRole, token)
|
resp, auResp, errs := usersCl.AddUser(userName, userPwd, userstore.UserRole, token)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
|
@ -100,6 +114,18 @@ func TestSingleUserHandlers(t *testing.T) {
|
||||||
// TODO: check id
|
// TODO: check id
|
||||||
fmt.Printf("new user id: %v\n", auResp)
|
fmt.Printf("new user id: %v\n", auResp)
|
||||||
|
|
||||||
|
// check uploading file
|
||||||
|
userFsRootFolder := q.FsRootPath(auResp.ID, "/")
|
||||||
|
_, err = fs.Stat(userFsRootFolder)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
userUploadFolder := q.UploadFolder(auResp.ID)
|
||||||
|
_, err = fs.Stat(userUploadFolder)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
resp, _, errs = usersCl.Logout(token)
|
resp, _, errs = usersCl.Logout(token)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
|
@ -114,6 +140,85 @@ func TestSingleUserHandlers(t *testing.T) {
|
||||||
t.Fatal(resp.StatusCode)
|
t.Fatal(resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resp, _, errs = usersCl.DelUser(auResp.ID, 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test users APIs: Login-AddUser-ListUsers-DelUser-ListUsers", 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, userRole := "user_admin", "1234", userstore.UserRole
|
||||||
|
resp, auResp, errs := usersCl.AddUser(userName, userPwd, 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)
|
||||||
|
newUserID, err := strconv.ParseUint(auResp.ID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, lsResp, errs := usersCl.ListUsers(token)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatal(errs)
|
||||||
|
} else if resp.StatusCode != 200 {
|
||||||
|
t.Fatal(resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lsResp.Users) != 2 {
|
||||||
|
t.Fatal(fmt.Errorf("incorrect users size (%d)", len(lsResp.Users)))
|
||||||
|
} else if lsResp.Users[0].ID != 0 ||
|
||||||
|
lsResp.Users[0].Name != adminName ||
|
||||||
|
lsResp.Users[0].Role != userstore.AdminRole {
|
||||||
|
t.Fatal(fmt.Errorf("incorrect root info (%v)", lsResp.Users[0]))
|
||||||
|
} else if lsResp.Users[1].ID != newUserID ||
|
||||||
|
lsResp.Users[1].Name != userName ||
|
||||||
|
lsResp.Users[1].Role != userRole {
|
||||||
|
t.Fatal(fmt.Errorf("incorrect user info (%v)", lsResp.Users[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, _, errs = usersCl.DelUser(auResp.ID, token)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatal(errs)
|
||||||
|
} else if resp.StatusCode != 200 {
|
||||||
|
t.Fatal(resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, lsResp, errs = usersCl.ListUsers(token)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatal(errs)
|
||||||
|
} else if resp.StatusCode != 200 {
|
||||||
|
t.Fatal(resp.StatusCode)
|
||||||
|
}
|
||||||
|
if len(lsResp.Users) != 1 {
|
||||||
|
t.Fatal(fmt.Errorf("incorrect users size (%d)", len(lsResp.Users)))
|
||||||
|
} else if lsResp.Users[0].ID != 0 ||
|
||||||
|
lsResp.Users[0].Name != adminName ||
|
||||||
|
lsResp.Users[0].Role != userstore.AdminRole {
|
||||||
|
t.Fatal(fmt.Errorf("incorrect root info (%v)", lsResp.Users[0]))
|
||||||
|
}
|
||||||
|
|
||||||
resp, _, errs = usersCl.Logout(token)
|
resp, _, errs = usersCl.Logout(token)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
// "path"
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ihexxa/gocfg"
|
"github.com/ihexxa/gocfg"
|
||||||
|
@ -64,3 +70,104 @@ func compareFileContent(fs fspkg.ISimpleFS, uid, filePath string, expectedConten
|
||||||
|
|
||||||
return string(gotContent) == expectedContent, nil
|
return string(gotContent) == expectedContent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertUploadOK(t *testing.T, filePath, content, addr string, token *http.Cookie) bool {
|
||||||
|
cl := client.NewFilesClient(addr, token)
|
||||||
|
|
||||||
|
fileSize := int64(len([]byte(content)))
|
||||||
|
res, _, errs := cl.Create(filePath, fileSize)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Error(errs)
|
||||||
|
return false
|
||||||
|
} else if res.StatusCode != 200 {
|
||||||
|
t.Error(res.StatusCode)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||||||
|
res, _, errs = cl.UploadChunk(filePath, base64Content, 0)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Error(errs)
|
||||||
|
return false
|
||||||
|
} else if res.StatusCode != 200 {
|
||||||
|
t.Error(res.StatusCode)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertDownloadOK(t *testing.T, filePath, content, addr string, token *http.Cookie) bool {
|
||||||
|
var (
|
||||||
|
res *http.Response
|
||||||
|
body string
|
||||||
|
errs []error
|
||||||
|
fileSize = int64(len([]byte(content)))
|
||||||
|
)
|
||||||
|
|
||||||
|
cl := client.NewFilesClient(addr, token)
|
||||||
|
|
||||||
|
rd := rand.Intn(3)
|
||||||
|
switch rd {
|
||||||
|
case 0:
|
||||||
|
res, body, errs = cl.Download(filePath, map[string]string{})
|
||||||
|
case 1:
|
||||||
|
res, body, errs = cl.Download(filePath, map[string]string{
|
||||||
|
"Range": fmt.Sprintf("bytes=0-%d", fileSize-1),
|
||||||
|
})
|
||||||
|
case 2:
|
||||||
|
res, body, errs = cl.Download(filePath, map[string]string{
|
||||||
|
"Range": fmt.Sprintf("bytes=0-%d, %d-%d", (fileSize-1)/2, (fileSize-1)/2+1, fileSize-1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := path.Base(filePath)
|
||||||
|
contentDispositionHeader := res.Header.Get("Content-Disposition")
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Error(errs)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if res.StatusCode != 200 && res.StatusCode != 206 {
|
||||||
|
t.Error(res.StatusCode)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if contentDispositionHeader != fmt.Sprintf(`attachment; filename="%s"`, fileName) {
|
||||||
|
t.Errorf("incorrect Content-Disposition header: %s", contentDispositionHeader)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rd {
|
||||||
|
case 0:
|
||||||
|
if body != content {
|
||||||
|
t.Errorf("body not equal got(%s) expect(%s)\n", body, content)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
if body[2:] != content { // body returned by gorequest contains the first CRLF
|
||||||
|
t.Errorf("body not equal got(%s) expect(%s)\n", body[2:], content)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
body = body[2:] // body returned by gorequest contains the first CRLF
|
||||||
|
realBody := ""
|
||||||
|
boundaryEnd := strings.Index(body, "\r\n")
|
||||||
|
boundary := body[0:boundaryEnd]
|
||||||
|
bodyParts := strings.Split(body, boundary)
|
||||||
|
|
||||||
|
for i, bodyPart := range bodyParts {
|
||||||
|
if i == 0 || i == len(bodyParts)-1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
start := strings.Index(bodyPart, "\r\n\r\n")
|
||||||
|
|
||||||
|
fmt.Printf("<%s>", bodyPart[start+4:len(bodyPart)-2]) // ignore the last CRLF
|
||||||
|
realBody += bodyPart[start+4 : len(bodyPart)-2]
|
||||||
|
}
|
||||||
|
if realBody != content {
|
||||||
|
t.Errorf("multi body not equal got(%s) expect(%s)\n", realBody, content)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
@ -26,20 +26,22 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint64
|
ID uint64 `json:"id,string"`
|
||||||
Name string
|
Name string `json:"name"`
|
||||||
Pwd string
|
Pwd string `json:"pwd"`
|
||||||
Role string
|
Role string `json:"role"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IUserStore interface {
|
type IUserStore interface {
|
||||||
Init(rootName, rootPwd string) error
|
Init(rootName, rootPwd string) error
|
||||||
IsInited() bool
|
IsInited() bool
|
||||||
AddUser(user *User) error
|
AddUser(user *User) error
|
||||||
|
DelUser(id uint64) error
|
||||||
GetUser(id uint64) (*User, error)
|
GetUser(id uint64) (*User, error)
|
||||||
GetUserByName(name string) (*User, error)
|
GetUserByName(name string) (*User, error)
|
||||||
SetName(id uint64, name string) error
|
SetName(id uint64, name string) error
|
||||||
SetPwd(id uint64, pwd string) error
|
SetPwd(id uint64, pwd string) error
|
||||||
|
ListUsers() ([]*User, error)
|
||||||
SetRole(id uint64, role string) error
|
SetRole(id uint64, role string) error
|
||||||
AddRole(role string) error
|
AddRole(role string) error
|
||||||
DelRole(role string) error
|
DelRole(role string) error
|
||||||
|
@ -135,6 +137,27 @@ func (us *KVUserStore) AddUser(user *User) error {
|
||||||
return us.store.SetStringIn(RolesNs, userID, user.Role)
|
return us.store.SetStringIn(RolesNs, userID, user.Role)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (us *KVUserStore) DelUser(id uint64) error {
|
||||||
|
us.mtx.Lock()
|
||||||
|
defer us.mtx.Unlock()
|
||||||
|
|
||||||
|
userID := fmt.Sprint(id)
|
||||||
|
name, ok := us.store.GetStringIn(NamesNs, userID)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("userID (%s) exists", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add complement operations if part of the actions fails
|
||||||
|
err1 := us.store.DelStringIn(NamesNs, userID)
|
||||||
|
err2 := us.store.DelStringIn(IDsNs, name)
|
||||||
|
err3 := us.store.DelStringIn(PwdsNs, userID)
|
||||||
|
err4 := us.store.DelStringIn(RolesNs, userID)
|
||||||
|
if err1 != nil || err2 != nil || err3 != nil || err4 != nil {
|
||||||
|
return fmt.Errorf("get name(%s) id(%s) pwd(%s) role(%s)", err1, err2, err3, err4)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (us *KVUserStore) GetUser(id uint64) (*User, error) {
|
func (us *KVUserStore) GetUser(id uint64) (*User, error) {
|
||||||
us.mtx.RLock()
|
us.mtx.RLock()
|
||||||
defer us.mtx.RUnlock()
|
defer us.mtx.RUnlock()
|
||||||
|
@ -258,6 +281,37 @@ func (us *KVUserStore) SetRole(id uint64, role string) error {
|
||||||
return us.store.SetStringIn(RolesNs, userID, role)
|
return us.store.SetStringIn(RolesNs, userID, role)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (us *KVUserStore) ListUsers() ([]*User, error) {
|
||||||
|
us.mtx.RLock()
|
||||||
|
defer us.mtx.RUnlock()
|
||||||
|
|
||||||
|
idToName, err := us.store.ListStringsIn(NamesNs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, err := us.store.ListStringsIn(RolesNs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
users := []*User{}
|
||||||
|
for id, name := range idToName {
|
||||||
|
intID, err := strconv.ParseUint(id, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
users = append(users, &User{
|
||||||
|
ID: intID,
|
||||||
|
Name: name,
|
||||||
|
Role: roles[id],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (us *KVUserStore) AddRole(role string) error {
|
func (us *KVUserStore) AddRole(role string) error {
|
||||||
us.mtx.Lock()
|
us.mtx.Lock()
|
||||||
defer us.mtx.Unlock()
|
defer us.mtx.Unlock()
|
||||||
|
|
|
@ -51,6 +51,20 @@ func TestUserStores(t *testing.T) {
|
||||||
t.Fatalf("roles not matched %s %s", role1, user.Role)
|
t.Fatalf("roles not matched %s %s", role1, user.Role)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
users, err := store.ListUsers()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(users) != 2 {
|
||||||
|
t.Fatalf("users size should be 2 (%d)", len(users))
|
||||||
|
}
|
||||||
|
if users[0].ID != 0 || users[0].Name != rootName || users[0].Role != AdminRole {
|
||||||
|
t.Fatalf("incorrect root info %v", users[0])
|
||||||
|
}
|
||||||
|
if users[1].ID != 1 || users[1].Name != name1 || users[1].Role != role1 {
|
||||||
|
t.Fatalf("incorrect user info %v", users[1])
|
||||||
|
}
|
||||||
|
|
||||||
err = store.SetName(id, name2)
|
err = store.SetName(id, name2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -91,6 +105,22 @@ func TestUserStores(t *testing.T) {
|
||||||
if user.Role != role2 {
|
if user.Role != role2 {
|
||||||
t.Fatalf("roles not matched %s %s", role2, user.Role)
|
t.Fatalf("roles not matched %s %s", role2, user.Role)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = store.DelUser(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
users, err = store.ListUsers()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(users) != 1 {
|
||||||
|
t.Fatalf("users size should be 2 (%d)", len(users))
|
||||||
|
}
|
||||||
|
if users[0].ID != 0 || users[0].Name != rootName || users[0].Role != AdminRole {
|
||||||
|
t.Fatalf("incorrect root info %v", users[0])
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testRoleMethods := func(t *testing.T, store IUserStore) {
|
testRoleMethods := func(t *testing.T, store IUserStore) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue