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;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 100;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#panes .container {
|
||||
#panes .root-container {
|
||||
max-width: 80rem;
|
||||
width: 96%;
|
||||
background-color: white;
|
||||
z-index: 101;
|
||||
text-align: left;
|
||||
margin: 3rem auto 8rem auto;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
|
||||
#panes .container {
|
||||
background-color: white;
|
||||
margin: 3rem auto 1rem auto;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
|
||||
#panes .return-btn {
|
||||
position: fixed;
|
||||
max-width: 960px;
|
||||
|
@ -133,12 +138,17 @@
|
|||
border-top: solid 1px transparent;
|
||||
}
|
||||
|
||||
#item-list .dot {
|
||||
.container .dot {
|
||||
overflow: hidden;
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
#panes .dot {
|
||||
overflow: hidden;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#item-list .vbar {
|
||||
overflow: hidden;
|
||||
margin: 1.5rem 1rem;
|
||||
|
@ -164,6 +174,14 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: break-word;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#item-list .item-op {
|
||||
line-height: 4rem;
|
||||
}
|
||||
|
|
|
@ -130,6 +130,22 @@ func (cl *FilesClient) List(dirPath string) (*http.Response, *fileshdr.ListResp,
|
|||
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) {
|
||||
resp, body, errs := cl.r.Get(cl.url("/v1/fs/uploadings")).
|
||||
AddCookie(cl.token).
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ihexxa/quickshare/src/handlers"
|
||||
"github.com/ihexxa/quickshare/src/handlers/multiusers"
|
||||
"github.com/parnurzeal/gorequest"
|
||||
)
|
||||
|
@ -74,6 +75,30 @@ func (cl *SingleUserClient) AddUser(name, pwd, role string, token *http.Cookie)
|
|||
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) {
|
||||
return cl.r.Post(cl.url("/v1/roles/")).
|
||||
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) {
|
||||
resp, body, errs := cl.r.Get(cl.url("/v1/roles/")).
|
||||
resp, body, errs := cl.r.Get(cl.url("/v1/roles/list")).
|
||||
AddCookie(token).
|
||||
End()
|
||||
if len(errs) > 0 {
|
||||
|
@ -108,3 +133,20 @@ func (cl *SingleUserClient) ListRoles(token *http.Cookie) (*http.Response, *mult
|
|||
}
|
||||
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";
|
||||
|
||||
export const defaultTimeout = 10000;
|
||||
export const userIDParam = "uid";
|
||||
|
||||
export interface User {
|
||||
ID: string;
|
||||
Name: string;
|
||||
Pwd: string;
|
||||
Role: string;
|
||||
id: string;
|
||||
name: string;
|
||||
pwd: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface ListUsersResp {
|
||||
users: Array<User>;
|
||||
}
|
||||
|
||||
export interface ListRolesResp {
|
||||
roles: Array<string>;
|
||||
}
|
||||
|
||||
export interface MetadataResp {
|
||||
|
@ -42,7 +51,15 @@ export interface IUsersClient {
|
|||
login: (user: string, pwd: string) => Promise<Response>;
|
||||
logout: () => Promise<Response>;
|
||||
isAuthed: () => Promise<Response>;
|
||||
self: () => 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 {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseClient, Response } from "./";
|
||||
import { BaseClient, Response, userIDParam } from "./";
|
||||
|
||||
export class UsersClient extends BaseClient {
|
||||
constructor(url: string) {
|
||||
|
@ -16,7 +16,6 @@ export class UsersClient extends BaseClient {
|
|||
});
|
||||
};
|
||||
|
||||
// token cookie is set by browser
|
||||
logout = (): Promise<Response> => {
|
||||
return this.do({
|
||||
method: "post",
|
||||
|
@ -31,7 +30,6 @@ export class UsersClient extends BaseClient {
|
|||
});
|
||||
};
|
||||
|
||||
// token cookie is set by browser
|
||||
setPwd = (oldPwd: string, newPwd: string): Promise<Response> => {
|
||||
return this.do({
|
||||
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
|
||||
adduser = (name: string, pwd: string, role: string): Promise<Response> => {
|
||||
addUser = (name: string, pwd: string, role: string): Promise<Response> => {
|
||||
return this.do({
|
||||
method: "post",
|
||||
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 isAuthedMockResp: Promise<Response>;
|
||||
private setPwdMockResp: Promise<Response>;
|
||||
private forceSetPwdMockResp: 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) {
|
||||
this.url = url;
|
||||
|
@ -25,9 +32,30 @@ export class MockUsersClient {
|
|||
setPwdMock = (resp: Promise<Response>) => {
|
||||
this.setPwdMockResp = resp;
|
||||
}
|
||||
forceSetPwdMock = (resp: Promise<Response>) => {
|
||||
this.forceSetPwdMockResp = resp;
|
||||
}
|
||||
addUserMock = (resp: Promise<Response>) => {
|
||||
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> => {
|
||||
return this.loginMockResp;
|
||||
|
@ -45,8 +73,35 @@ export class MockUsersClient {
|
|||
return this.setPwdMockResp;
|
||||
}
|
||||
|
||||
forceSetPwd = (userID: string, newPwd: string): Promise<Response> => {
|
||||
return this.forceSetPwdMockResp;
|
||||
}
|
||||
|
||||
addUser = (name: string, pwd: string, role: string): Promise<Response> => {
|
||||
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 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 = (
|
||||
<div>
|
||||
<div>
|
||||
|
|
|
@ -124,17 +124,6 @@ export class Updater {
|
|||
: 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 (
|
||||
srcDir: string,
|
||||
dstDir: string,
|
||||
|
|
|
@ -60,16 +60,17 @@ export function initState(): ICoreState {
|
|||
uploadFiles: List<File>([]),
|
||||
},
|
||||
panes: {
|
||||
userRole: "",
|
||||
displaying: "",
|
||||
paneNames: Set<string>(["settings", "login"]),
|
||||
paneNames: Set<string>(["settings", "login", "admin"]),
|
||||
login: {
|
||||
authed: false,
|
||||
},
|
||||
admin: {
|
||||
users: Map<string, User>(),
|
||||
roles: Set<string>(),
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
users: Map<string, User>(),
|
||||
roles: Set<string>()
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -92,16 +93,17 @@ export function mockState(): ICoreState {
|
|||
uploadFiles: List<File>([]),
|
||||
},
|
||||
panes: {
|
||||
userRole: "",
|
||||
displaying: "",
|
||||
paneNames: Set<string>(["settings", "login"]),
|
||||
paneNames: Set<string>(["settings", "login", "admin"]),
|
||||
login: {
|
||||
authed: false,
|
||||
},
|
||||
admin: {
|
||||
users: Map<string, User>(),
|
||||
roles: Set<string>(),
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
users: Map<string, User>(),
|
||||
roles: Set<string>()
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,11 +2,8 @@ import * as React from "react";
|
|||
import { Map, Set } from "immutable";
|
||||
|
||||
import { ICoreState } from "./core_state";
|
||||
import { IUsersClient, User} from "../client";
|
||||
import { UsersClient } from "../client/users";
|
||||
import { User } from "../client";
|
||||
import { Updater as PanesUpdater } from "./panes";
|
||||
import { updater as BrowserUpdater } from "./browser.updater";
|
||||
import { Layouter } from "./layouter";
|
||||
|
||||
export interface Props {
|
||||
users: Map<string, User>;
|
||||
|
@ -14,165 +11,408 @@ export interface Props {
|
|||
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||
}
|
||||
|
||||
export class Updater {
|
||||
private static props: Props;
|
||||
private static client: IUsersClient;
|
||||
|
||||
static init = (props: Props) => (Updater.props = { ...props });
|
||||
|
||||
static setClient = (client: IUsersClient): void => {
|
||||
Updater.client = client;
|
||||
};
|
||||
|
||||
// static adduser = async (user: User): Promise<boolean> => {
|
||||
// const resp = await Updater.client.add
|
||||
// }
|
||||
|
||||
// static login = async (user: string, pwd: string): Promise<boolean> => {
|
||||
// const resp = await Updater.client.login(user, pwd);
|
||||
// Updater.setAuthed(resp.status === 200);
|
||||
// return resp.status === 200;
|
||||
// };
|
||||
|
||||
// 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 UserFormProps {
|
||||
key: string;
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
roles: Set<string>;
|
||||
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||
}
|
||||
|
||||
// export interface State {
|
||||
// user: string;
|
||||
// pwd: string;
|
||||
// }
|
||||
export interface UserFormState {
|
||||
id: string;
|
||||
name: string;
|
||||
newPwd1: string;
|
||||
newPwd2: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
// export class AuthPane extends React.Component<Props, State, {}> {
|
||||
// private update: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||
// constructor(p: Props) {
|
||||
// super(p);
|
||||
// Updater.init(p);
|
||||
// Updater.setClient(new UsersClient(""));
|
||||
// this.update = p.update;
|
||||
// this.state = {
|
||||
// user: "",
|
||||
// pwd: "",
|
||||
// };
|
||||
export class UserForm extends React.Component<
|
||||
UserFormProps,
|
||||
UserFormState,
|
||||
{}
|
||||
> {
|
||||
constructor(p: UserFormProps) {
|
||||
super(p);
|
||||
this.state = {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
newPwd1: "",
|
||||
newPwd2: "",
|
||||
role: p.role,
|
||||
};
|
||||
}
|
||||
|
||||
// this.initIsAuthed();
|
||||
// }
|
||||
changePwd1 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ newPwd1: ev.target.value });
|
||||
};
|
||||
changePwd2 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ newPwd2: ev.target.value });
|
||||
};
|
||||
changeRole = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ role: ev.target.value });
|
||||
};
|
||||
|
||||
// changeUser = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// this.setState({ user: ev.target.value });
|
||||
// };
|
||||
setPwd = () => {
|
||||
if (this.state.newPwd1 !== this.state.newPwd2) {
|
||||
alert("2 passwords do not match, please check.");
|
||||
return;
|
||||
}
|
||||
|
||||
// changePwd = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// this.setState({ pwd: ev.target.value });
|
||||
// };
|
||||
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: "",
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// initIsAuthed = () => {
|
||||
// Updater.initIsAuthed().then(() => {
|
||||
// this.update(Updater.setAuthPane);
|
||||
// });
|
||||
// };
|
||||
delUser = () => {
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
// login = () => {
|
||||
// Updater.login(this.state.user, this.state.pwd)
|
||||
// .then((ok: boolean) => {
|
||||
// if (ok) {
|
||||
// this.update(Updater.setAuthPane);
|
||||
// this.setState({ user: "", pwd: "" });
|
||||
// // close all the panes
|
||||
// PanesUpdater.displayPane("");
|
||||
// this.update(PanesUpdater.updateState);
|
||||
// setRole = () => {};
|
||||
|
||||
// // refresh
|
||||
// return BrowserUpdater().setHomeItems();
|
||||
// } else {
|
||||
// this.setState({ user: "", pwd: "" });
|
||||
// alert("Failed to login.");
|
||||
// }
|
||||
// })
|
||||
// .then(() => {
|
||||
// return BrowserUpdater().refreshUploadings();
|
||||
// })
|
||||
// .then((_: boolean) => {
|
||||
// this.update(BrowserUpdater().setBrowser);
|
||||
// });
|
||||
// };
|
||||
render() {
|
||||
return (
|
||||
<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>
|
||||
|
||||
// logout = () => {
|
||||
// Updater.logout().then((ok: boolean) => {
|
||||
// if (ok) {
|
||||
// this.update(Updater.setAuthPane);
|
||||
// } else {
|
||||
// alert("Failed to logout.");
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
{/* no API yet */}
|
||||
{/* <div className="margin-t-m">
|
||||
<input
|
||||
name={`${this.props.id}-role`}
|
||||
type="text"
|
||||
onChange={this.changeRole}
|
||||
value={this.state.role}
|
||||
className="black0-font margin-r-m"
|
||||
placeholder={this.props.role}
|
||||
/>
|
||||
<button
|
||||
onClick={this.setRole}
|
||||
className="grey1-bg white-font margin-r-m"
|
||||
>
|
||||
Update Role
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
</span>
|
||||
|
||||
// render() {
|
||||
// const elements: Array<JSX.Element> = [
|
||||
// <input
|
||||
// name="user"
|
||||
// type="text"
|
||||
// onChange={this.changeUser}
|
||||
// value={this.state.user}
|
||||
// className="black0-font margin-t-m margin-b-m"
|
||||
// // style={{ width: "80%" }}
|
||||
// placeholder="user name"
|
||||
// />,
|
||||
// <input
|
||||
// name="pwd"
|
||||
// type="password"
|
||||
// onChange={this.changePwd}
|
||||
// value={this.state.pwd}
|
||||
// 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>,
|
||||
// ];
|
||||
<div className="margin-t-m">
|
||||
<input
|
||||
name={`${this.props.id}-pwd1`}
|
||||
type="password"
|
||||
onChange={this.changePwd1}
|
||||
value={this.state.newPwd1}
|
||||
className="black0-font margin-r-m"
|
||||
placeholder="new password"
|
||||
/>
|
||||
<input
|
||||
name={`${this.props.id}-pwd2`}
|
||||
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>
|
||||
|
||||
// return (
|
||||
// <span>
|
||||
// <div
|
||||
// className="margin-l-l"
|
||||
// style={{ display: this.props.authed ? "none" : "block" }}
|
||||
// >
|
||||
// {/* <h5 className="black-font">Login</h5> */}
|
||||
// <Layouter isHorizontal={false} elements={elements} />
|
||||
// </div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// <span style={{ display: this.props.authed ? "inherit" : "none" }}>
|
||||
// <button onClick={this.logout} className="grey1-bg white-font">
|
||||
// Log out
|
||||
// </button>
|
||||
// </span>
|
||||
// </span>
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
export interface State {
|
||||
newUserName: string;
|
||||
newUserPwd1: string;
|
||||
newUserPwd2: string;
|
||||
newUserRole: string;
|
||||
newRole: string;
|
||||
}
|
||||
export class AdminPane extends React.Component<Props, State, {}> {
|
||||
constructor(p: Props) {
|
||||
super(p);
|
||||
this.state = {
|
||||
newUserName: "",
|
||||
newUserPwd1: "",
|
||||
newUserPwd2: "",
|
||||
newUserRole: "",
|
||||
newRole: "",
|
||||
};
|
||||
}
|
||||
|
||||
onChangeUserName = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ newUserName: ev.target.value });
|
||||
};
|
||||
onChangeUserPwd1 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ newUserPwd1: ev.target.value });
|
||||
};
|
||||
onChangeUserPwd2 = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ newUserPwd2: ev.target.value });
|
||||
};
|
||||
onChangeUserRole = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ newUserRole: ev.target.value });
|
||||
};
|
||||
onChangeRole = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ newRole: ev.target.value });
|
||||
};
|
||||
|
||||
addRole = () => {
|
||||
PanesUpdater.addRole(this.state.newRole)
|
||||
.then((ok: boolean) => {
|
||||
if (!ok) {
|
||||
alert("failed to add role");
|
||||
}
|
||||
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,11 +159,13 @@ export class AuthPane extends React.Component<Props, State, {}> {
|
|||
return (
|
||||
<span>
|
||||
<div
|
||||
className="margin-l-l"
|
||||
className="container"
|
||||
style={{ display: this.props.authed ? "none" : "block" }}
|
||||
>
|
||||
{/* <h5 className="black-font">Login</h5> */}
|
||||
<Layouter isHorizontal={false} elements={elements} />
|
||||
<div className="padding-l">
|
||||
{/* <h5 className="black-font">Login</h5> */}
|
||||
<Layouter isHorizontal={false} elements={elements} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span style={{ display: this.props.authed ? "inherit" : "none" }}>
|
||||
|
|
|
@ -120,58 +120,63 @@ export class PaneSettings extends React.Component<Props, State, {}> {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="padding-l">
|
||||
<div>
|
||||
<div className="flex-list-container">
|
||||
<div className="flex-list-item-l">
|
||||
<h5 className="black-font">Update Password</h5>
|
||||
<div className="container">
|
||||
<div className="padding-l">
|
||||
<div>
|
||||
<div className="flex-list-container">
|
||||
<div className="flex-list-item-l">
|
||||
<h5 className="black-font">Update Password</h5>
|
||||
</div>
|
||||
<div className="flex-list-item-r">
|
||||
<button onClick={this.setPwd} className="grey1-bg white-font">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-list-item-r">
|
||||
<button onClick={this.setPwd} className="grey1-bg white-font">
|
||||
Update
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<input
|
||||
name="old_pwd"
|
||||
type="password"
|
||||
onChange={this.changeOldPwd}
|
||||
value={this.state.oldPwd}
|
||||
className="black0-font margin-t-m margin-b-m"
|
||||
placeholder="old password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
name="new_pwd1"
|
||||
type="password"
|
||||
onChange={this.changeNewPwd1}
|
||||
value={this.state.newPwd1}
|
||||
className="black0-font margin-t-m margin-b-m margin-r-m"
|
||||
placeholder="new password"
|
||||
/>
|
||||
<input
|
||||
name="new_pwd2"
|
||||
type="password"
|
||||
onChange={this.changeNewPwd2}
|
||||
value={this.state.newPwd2}
|
||||
className="black0-font margin-t-m margin-b-m"
|
||||
placeholder="new password again"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hr white0-bg margin-t-m margin-b-m"></div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
name="old_pwd"
|
||||
type="password"
|
||||
onChange={this.changeOldPwd}
|
||||
value={this.state.oldPwd}
|
||||
className="black0-font margin-t-m margin-b-m"
|
||||
placeholder="old password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
name="new_pwd1"
|
||||
type="password"
|
||||
onChange={this.changeNewPwd1}
|
||||
value={this.state.newPwd1}
|
||||
className="black0-font margin-t-m margin-b-m margin-r-m"
|
||||
placeholder="new password"
|
||||
/>
|
||||
<input
|
||||
name="new_pwd2"
|
||||
type="password"
|
||||
onChange={this.changeNewPwd2}
|
||||
value={this.state.newPwd2}
|
||||
className="black0-font margin-t-m margin-b-m"
|
||||
placeholder="new password again"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hr white0-bg margin-t-m margin-b-m"></div>
|
||||
|
||||
<div>
|
||||
<div className="flex-list-container">
|
||||
<div className="flex-list-item-l">
|
||||
<h5 className="black-font">Logout</h5>
|
||||
</div>
|
||||
<div className="flex-list-item-r">
|
||||
<AuthPane authed={this.props.login.authed} update={this.update} />
|
||||
<div className="flex-list-container">
|
||||
<div className="flex-list-item-l">
|
||||
<h5 className="black-font">Logout</h5>
|
||||
</div>
|
||||
<div className="flex-list-item-r">
|
||||
<AuthPane
|
||||
authed={this.props.login.authed}
|
||||
update={this.update}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,21 +1,30 @@
|
|||
import * as React from "react";
|
||||
import { Set, Map } from "immutable";
|
||||
|
||||
import { IUsersClient, User, ListUsersResp, ListRolesResp } from "../client";
|
||||
import { UsersClient } from "../client/users";
|
||||
import { ICoreState } from "./core_state";
|
||||
import { PaneSettings } from "./pane_settings";
|
||||
import { AdminPane, Props as AdminPaneProps } from "./pane_admin";
|
||||
import { AuthPane, Props as AuthPaneProps } from "./pane_login";
|
||||
|
||||
export interface Props {
|
||||
userRole: string;
|
||||
displaying: string;
|
||||
paneNames: Set<string>;
|
||||
login: AuthPaneProps;
|
||||
admin: AdminPaneProps;
|
||||
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||
}
|
||||
|
||||
export class Updater {
|
||||
static props: Props;
|
||||
private static client: IUsersClient;
|
||||
|
||||
static init = (props: Props) => (Updater.props = { ...props });
|
||||
static setClient = (client: IUsersClient): void => {
|
||||
Updater.client = client;
|
||||
};
|
||||
|
||||
static displayPane = (paneName: string) => {
|
||||
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 => {
|
||||
console.log(prevState, Updater.props);
|
||||
return {
|
||||
...prevState,
|
||||
panel: {
|
||||
|
@ -47,6 +134,7 @@ export class Panes extends React.Component<Props, State, {}> {
|
|||
constructor(p: Props) {
|
||||
super(p);
|
||||
Updater.init(p);
|
||||
Updater.setClient(new UsersClient(""));
|
||||
}
|
||||
|
||||
closePane = () => {
|
||||
|
@ -63,7 +151,7 @@ export class Panes extends React.Component<Props, State, {}> {
|
|||
displaying = "login";
|
||||
}
|
||||
|
||||
const panesMap: Map<string, JSX.Element> = Map({
|
||||
let panesMap: Map<string, JSX.Element> = Map({
|
||||
settings: (
|
||||
<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 isDisplay = displaying === paneName ? "" : "hidden";
|
||||
return (
|
||||
|
@ -84,24 +183,25 @@ export class Panes extends React.Component<Props, State, {}> {
|
|||
const btnClass = displaying === "login" ? "hidden" : "";
|
||||
return (
|
||||
<div id="panes" className={displaying === "" ? "hidden" : ""}>
|
||||
<div className="container">
|
||||
<div className="flex-list-container padding-l">
|
||||
<h3 className="flex-list-item-l txt-cap">{displaying}</h3>
|
||||
<div className="flex-list-item-r">
|
||||
<button
|
||||
onClick={this.closePane}
|
||||
className={`black0-bg white-font ${btnClass}`}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<div className="root-container">
|
||||
<div className="container">
|
||||
<div className="flex-list-container padding-l">
|
||||
<h3 className="flex-list-item-l txt-cap">{displaying}</h3>
|
||||
<div className="flex-list-item-r">
|
||||
<button
|
||||
onClick={this.closePane}
|
||||
className={`red0-bg white-font ${btnClass}`}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</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 className="hr white0-bg margin-b-m margin-l-m margin-r-m"></div> */}
|
||||
{/* <div className="padding-l"></div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ export interface Props {
|
|||
browser: BrowserProps;
|
||||
authPane: PaneLoginProps;
|
||||
panes: PanesProps;
|
||||
admin: PaneAdminProps;
|
||||
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
|
||||
}
|
||||
|
||||
|
@ -38,15 +37,22 @@ export class RootFrame extends React.Component<Props, State, {}> {
|
|||
this.props.update(PanesUpdater.updateState);
|
||||
};
|
||||
|
||||
showAdmin = () => {
|
||||
PanesUpdater.displayPane("admin");
|
||||
this.props.update(PanesUpdater.updateState);
|
||||
};
|
||||
|
||||
render() {
|
||||
const update = this.props.update;
|
||||
return (
|
||||
<div className="theme-white desktop">
|
||||
<div id="bg" className="bg bg-img font-m">
|
||||
<Panes
|
||||
userRole={this.props.panes.userRole}
|
||||
displaying={this.props.panes.displaying}
|
||||
paneNames={this.props.panes.paneNames}
|
||||
login={this.props.authPane}
|
||||
admin={this.props.panes.admin}
|
||||
update={update}
|
||||
/>
|
||||
|
||||
|
@ -68,6 +74,12 @@ export class RootFrame extends React.Component<Props, State, {}> {
|
|||
>
|
||||
Settings
|
||||
</button>
|
||||
<button
|
||||
onClick={this.showAdmin}
|
||||
className="grey1-bg white-font margin-r-m"
|
||||
>
|
||||
Admin
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { updater as BrowserUpdater } from "./browser.updater";
|
||||
import { Updater as PanesUpdater } from "./panes";
|
||||
import { ICoreState, init } from "./core_state";
|
||||
import { RootFrame } from "./root_frame";
|
||||
import { FilesClient } from "../client/files";
|
||||
|
@ -26,6 +27,19 @@ export class StateMgr extends React.Component<Props, State, {}> {
|
|||
})
|
||||
.then((_: boolean) => {
|
||||
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}
|
||||
browser={this.state.panel.browser}
|
||||
panes={this.state.panel.panes}
|
||||
admin={this.state.panel.admin}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ func (h *FileHandlers) Create(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
tmpFilePath := q.GetTmpPath(userID, req.Path)
|
||||
tmpFilePath := q.UploadPath(userID, req.Path)
|
||||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||||
locker.Exec(func() {
|
||||
err := h.deps.FS().Create(tmpFilePath)
|
||||
|
@ -295,7 +295,7 @@ func (h *FileHandlers) UploadChunk(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
tmpFilePath := q.GetTmpPath(userID, req.Path)
|
||||
tmpFilePath := q.UploadPath(userID, req.Path)
|
||||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||||
locker.Exec(func() {
|
||||
var err error
|
||||
|
@ -407,7 +407,7 @@ func (h *FileHandlers) UploadStatus(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
tmpFilePath := q.GetTmpPath(userID, filePath)
|
||||
tmpFilePath := q.UploadPath(userID, filePath)
|
||||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||||
locker.Exec(func() {
|
||||
_, 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) {
|
||||
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 {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
|
@ -565,7 +566,7 @@ func (h *FileHandlers) ListHome(c *gin.Context) {
|
|||
}
|
||||
|
||||
c.JSON(200, &ListResp{
|
||||
Cwd: userID,
|
||||
Cwd: fsPath,
|
||||
Metadatas: metadatas,
|
||||
})
|
||||
}
|
||||
|
@ -606,7 +607,7 @@ func (h *FileHandlers) DelUploading(c *gin.Context) {
|
|||
userID := c.MustGet(q.UserIDParam).(string)
|
||||
|
||||
var err error
|
||||
tmpFilePath := q.GetTmpPath(userID, filePath)
|
||||
tmpFilePath := q.UploadPath(userID, filePath)
|
||||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||||
locker.Exec(func() {
|
||||
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) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -33,36 +33,41 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error)
|
|||
apiACRules := map[string]bool{
|
||||
// TODO: make these configurable
|
||||
// admin rules
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", publicPath): true,
|
||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/login"): true,
|
||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/logout"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/users/isauthed"): true,
|
||||
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/users/pwd"): true,
|
||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/"): true,
|
||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/roles/"): true,
|
||||
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/roles/"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/roles/"): true,
|
||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/files"): true,
|
||||
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/files"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files"): true,
|
||||
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/chunks"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files/chunks"): true,
|
||||
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/copy"): true,
|
||||
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/move"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/dirs"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/dirs/home"): true,
|
||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/dirs"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/uploadings"): true,
|
||||
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/uploadings"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/metadata"): true,
|
||||
apiRuleCname(userstore.AdminRole, "OPTIONS", "/v1/settings/health"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", publicPath): true,
|
||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/login"): true,
|
||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/logout"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/users/isauthed"): 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, "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, "DELETE", "/v1/roles/"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/roles/list"): true,
|
||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/files"): true,
|
||||
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/files"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files"): true,
|
||||
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/chunks"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files/chunks"): true,
|
||||
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/copy"): true,
|
||||
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/move"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/dirs"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/dirs/home"): true,
|
||||
apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/dirs"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/uploadings"): true,
|
||||
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/uploadings"): true,
|
||||
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/metadata"): true,
|
||||
apiRuleCname(userstore.AdminRole, "OPTIONS", "/v1/settings/health"): true,
|
||||
// user rules
|
||||
apiRuleCname(userstore.UserRole, "GET", "/"): true,
|
||||
apiRuleCname(userstore.UserRole, "GET", publicPath): true,
|
||||
apiRuleCname(userstore.UserRole, "POST", "/v1/users/logout"): true,
|
||||
apiRuleCname(userstore.UserRole, "GET", "/v1/users/isauthed"): 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, "DELETE", "/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, "POST", "/v1/users/login"): 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, "OPTIONS", "/v1/settings/health"): true,
|
||||
}
|
||||
|
@ -97,12 +103,12 @@ func (h *MultiUsersSvc) Init(adminName, adminPwd string) (string, error) {
|
|||
var err error
|
||||
|
||||
userID := "0"
|
||||
fsPath := q.HomePath(userID, "/")
|
||||
fsPath := q.FsRootPath(userID, "/")
|
||||
if err = h.deps.FS().MkdirAll(fsPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
uploadingsPath := q.GetTmpPath(userID, "/")
|
||||
if err = h.deps.FS().MkdirAll(uploadingsPath); err != nil {
|
||||
uploadFolder := q.UploadFolder(userID)
|
||||
if err = h.deps.FS().MkdirAll(uploadFolder); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
@ -231,6 +237,57 @@ func (h *MultiUsersSvc) SetPwd(c *gin.Context) {
|
|||
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 {
|
||||
Name string `json:"name"`
|
||||
Pwd string `json:"pwd"`
|
||||
|
@ -267,14 +324,14 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) {
|
|||
|
||||
// TODO: following operations must be atomic
|
||||
// TODO: check if the folders already exists
|
||||
userID := c.MustGet(q.UserIDParam).(string)
|
||||
homePath := q.HomePath(userID, "/")
|
||||
if err = h.deps.FS().MkdirAll(homePath); err != nil {
|
||||
uidStr := fmt.Sprint(uid)
|
||||
fsRootFolder := q.FsRootPath(uidStr, "/")
|
||||
if err = h.deps.FS().MkdirAll(fsRootFolder); err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
uploadingsPath := q.GetTmpPath(userID, "/")
|
||||
if err = h.deps.FS().MkdirAll(uploadingsPath); err != nil {
|
||||
uploadFolder := q.UploadFolder(uidStr)
|
||||
if err = h.deps.FS().MkdirAll(uploadFolder); err != nil {
|
||||
c.JSON(q.ErrResp(c, 500, err))
|
||||
return
|
||||
}
|
||||
|
@ -293,6 +350,71 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) {
|
|||
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 {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
@ -405,3 +527,23 @@ func (h *MultiUsersSvc) isValidRole(role string) error {
|
|||
}
|
||||
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
|
||||
UploadDir = "uploadings"
|
||||
FsDir = "files"
|
||||
FsRootDir = "files"
|
||||
|
||||
UserIDParam = "uid"
|
||||
UserParam = "user"
|
||||
|
@ -21,6 +22,7 @@ var (
|
|||
RoleParam = "role"
|
||||
ExpireParam = "expire"
|
||||
TokenCookie = "tk"
|
||||
LastID = "lid"
|
||||
|
||||
ErrAccessDenied = errors.New("access denied")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
|
@ -131,6 +133,18 @@ func HomePath(userID, relFilePath string) string {
|
|||
return filepath.Join(userID, relFilePath)
|
||||
}
|
||||
|
||||
func FsRootPath(userID, relFilePath string) string {
|
||||
return filepath.Join(userID, FsRootDir, relFilePath)
|
||||
}
|
||||
|
||||
func GetTmpPath(userID, relFilePath string) string {
|
||||
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 {
|
||||
return bp.db.Close()
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ var ErrNoLock = errors.New("no lock to unlock")
|
|||
type IKVStore interface {
|
||||
AddNamespace(nsName string) error
|
||||
DelNamespace(nsName string) error
|
||||
HasNamespace(nsName string) bool
|
||||
GetBool(key string) (bool, bool)
|
||||
GetBoolIn(ns, key string) (bool, bool)
|
||||
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.GET("/isauthed", userHdrs.IsAuthed)
|
||||
usersAPI.PATCH("/pwd", userHdrs.SetPwd)
|
||||
usersAPI.PATCH("/pwd/force-set", userHdrs.ForceSetPwd)
|
||||
usersAPI.POST("/", userHdrs.AddUser)
|
||||
usersAPI.DELETE("/", userHdrs.DelUser)
|
||||
usersAPI.GET("/list", userHdrs.ListUsers)
|
||||
usersAPI.GET("/self", userHdrs.Self)
|
||||
|
||||
rolesAPI := v1.Group("/roles")
|
||||
rolesAPI.POST("/", userHdrs.AddRole)
|
||||
rolesAPI.DELETE("/", userHdrs.DelRole)
|
||||
rolesAPI.GET("/", userHdrs.ListRoles)
|
||||
rolesAPI.GET("/list", userHdrs.ListRoles)
|
||||
|
||||
filesAPI := v1.Group("/fs")
|
||||
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
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
|
@ -65,120 +61,67 @@ func TestFileHandlers(t *testing.T) {
|
|||
token := client.GetCookie(resp.Cookies(), q.TokenCookie)
|
||||
cl := client.NewFilesClient(addr, token)
|
||||
|
||||
assertUploadOK := func(t *testing.T, filePath, content string) bool {
|
||||
cl := client.NewFilesClient(addr, token)
|
||||
// TODO: remove all files under home folder before testing
|
||||
// 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)))
|
||||
res, _, errs := cl.Create(filePath, fileSize)
|
||||
for filePath, content := range files {
|
||||
assertUploadOK(t, filePath, content, addr, token)
|
||||
|
||||
err = fs.Sync()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
resp, lhResp, errs := cl.ListHome()
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
t.Fatal(errs)
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Fatal(resp.StatusCode)
|
||||
} else if lhResp.Cwd != "0/files" {
|
||||
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))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
infos := map[string]*fileshdr.MetadataResp{}
|
||||
for _, metadata := range lhResp.Metadatas {
|
||||
infos[metadata.Name] = metadata
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test uploading files with duplicated names", func(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"0/dupdir/dup_file1": "12345678",
|
||||
"0/dupdir/dup_file2.ext": "12345678",
|
||||
"0/files/dupdir/dup_file1": "12345678",
|
||||
"0/files/dupdir/dup_file2.ext": "12345678",
|
||||
}
|
||||
renames := map[string]string{
|
||||
"0/dupdir/dup_file1": "0/dupdir/dup_file1_1",
|
||||
"0/dupdir/dup_file2.ext": "0/dupdir/dup_file2_1.ext",
|
||||
"0/files/dupdir/dup_file1": "0/files/dupdir/dup_file1_1",
|
||||
"0/files/dupdir/dup_file2.ext": "0/files/dupdir/dup_file2_1.ext",
|
||||
}
|
||||
|
||||
for filePath, content := range files {
|
||||
for i := 0; i < 2; i++ {
|
||||
assertUploadOK(t, filePath, content)
|
||||
assertUploadOK(t, filePath, content, addr, token)
|
||||
|
||||
err = fs.Sync()
|
||||
if err != nil {
|
||||
|
@ -186,13 +129,13 @@ func TestFileHandlers(t *testing.T) {
|
|||
}
|
||||
|
||||
if i == 0 {
|
||||
assetDownloadOK(t, filePath, content)
|
||||
assertDownloadOK(t, filePath, content, addr, token)
|
||||
} else if i == 1 {
|
||||
renamedFilePath, ok := renames[filePath]
|
||||
if !ok {
|
||||
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) {
|
||||
for filePath, content := range map[string]string{
|
||||
"0/path1/f1.md": "1111 1111 1111 1111",
|
||||
"0/path1/path2/f2.md": "1010 1010 1111 0000 0010",
|
||||
"0/files/path1/f1.md": "1111 1111 1111 1111",
|
||||
"0/files/path1/path2/f2.md": "1010 1010 1111 0000 0010",
|
||||
} {
|
||||
fileSize := int64(len([]byte(content)))
|
||||
// create a file
|
||||
|
@ -213,7 +156,7 @@ func TestFileHandlers(t *testing.T) {
|
|||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
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) {
|
||||
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",
|
||||
"f2.md": "22222222222",
|
||||
},
|
||||
"0/dir/path2/path2": map[string]string{
|
||||
"0/files/dir/path2/path2": map[string]string{
|
||||
"f3.md": "3333333",
|
||||
},
|
||||
} {
|
||||
|
@ -307,7 +250,7 @@ func TestFileHandlers(t *testing.T) {
|
|||
|
||||
for fileName, content := range files {
|
||||
filePath := filepath.Join(dirPath, fileName)
|
||||
assertUploadOK(t, filePath, content)
|
||||
assertUploadOK(t, filePath, content, addr, token)
|
||||
}
|
||||
|
||||
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) {
|
||||
srcDir := "0/move/src"
|
||||
dstDir := "0/move/dst"
|
||||
srcDir := "0/files/move/src"
|
||||
dstDir := "0/files/move/dst"
|
||||
|
||||
for _, dirPath := range []string{srcDir, dstDir} {
|
||||
res, _, errs := cl.Mkdir(dirPath)
|
||||
|
@ -352,7 +295,7 @@ func TestFileHandlers(t *testing.T) {
|
|||
oldPath := filepath.Join(srcDir, fileName)
|
||||
newPath := filepath.Join(dstDir, fileName)
|
||||
// fileSize := int64(len([]byte(content)))
|
||||
assertUploadOK(t, oldPath, content)
|
||||
assertUploadOK(t, oldPath, content, addr, token)
|
||||
|
||||
res, _, errs := cl.Move(oldPath, newPath)
|
||||
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) {
|
||||
for filePath, content := range map[string]string{
|
||||
"0/download/path1/f1": "123456",
|
||||
"0/download/path1/path2": "12345678",
|
||||
"0/files/download/path1/f1": "123456",
|
||||
"0/files/download/path1/path2": "12345678",
|
||||
} {
|
||||
assertUploadOK(t, filePath, content)
|
||||
assertUploadOK(t, filePath, content, addr, token)
|
||||
|
||||
err = fs.Sync()
|
||||
if err != nil {
|
||||
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) {
|
||||
for i := 0; i < 5; i++ {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -416,7 +359,7 @@ func TestFileHandlers(t *testing.T) {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
@ -446,10 +389,18 @@ func TestFileHandlers(t *testing.T) {
|
|||
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{
|
||||
"0/uploadings/path1/f1": "123456",
|
||||
"0/uploadings/path1/path2": "12345678",
|
||||
"0/files/uploadings/path1/f1": "123456",
|
||||
"0/files/uploadings/path1/path2": "12345678",
|
||||
}
|
||||
|
||||
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 {
|
||||
t.Fatal(errs)
|
||||
} else if res.StatusCode != 200 {
|
||||
|
@ -507,7 +458,7 @@ func TestFileHandlers(t *testing.T) {
|
|||
// cl := client.NewFilesClient(addr)
|
||||
|
||||
files := map[string]string{
|
||||
"0/uploadings/path1/f1": "12345678",
|
||||
"0/files/uploadings/path1/f1": "12345678",
|
||||
}
|
||||
|
||||
for filePath, content := range files {
|
||||
|
@ -554,7 +505,7 @@ func TestFileHandlers(t *testing.T) {
|
|||
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)
|
||||
|
||||
files := map[string]string{
|
||||
"0/uploadings/random/path1/f1": "12345678",
|
||||
"0/uploadings/random/path1/f2": "87654321",
|
||||
"0/uploadings/random/path1/f3": "17654321",
|
||||
"0/files/uploadings/random/path1/f1": "12345678",
|
||||
"0/files/uploadings/random/path1/f2": "87654321",
|
||||
"0/files/uploadings/random/path1/f3": "17654321",
|
||||
}
|
||||
|
||||
for filePath, content := range files {
|
||||
|
@ -622,7 +573,7 @@ func TestFileHandlers(t *testing.T) {
|
|||
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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/ihexxa/quickshare/src/client"
|
||||
q "github.com/ihexxa/quickshare/src/handlers"
|
||||
su "github.com/ihexxa/quickshare/src/handlers/singleuserhdr"
|
||||
"github.com/ihexxa/quickshare/src/userstore"
|
||||
)
|
||||
|
||||
func TestSingleUserHandlers(t *testing.T) {
|
||||
func TestUsersHandlers(t *testing.T) {
|
||||
addr := "http://127.0.0.1:8686"
|
||||
root := "testData"
|
||||
config := `{
|
||||
|
@ -41,6 +43,7 @@ func TestSingleUserHandlers(t *testing.T) {
|
|||
|
||||
srv := startTestServer(config)
|
||||
defer srv.Shutdown()
|
||||
fs := srv.depsFS()
|
||||
|
||||
usersCl := client.NewSingleUserClient(addr)
|
||||
|
||||
|
@ -48,7 +51,7 @@ func TestSingleUserHandlers(t *testing.T) {
|
|||
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)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
|
@ -58,6 +61,17 @@ func TestSingleUserHandlers(t *testing.T) {
|
|||
|
||||
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)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
|
@ -90,7 +104,7 @@ func TestSingleUserHandlers(t *testing.T) {
|
|||
|
||||
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)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
|
@ -100,6 +114,18 @@ func TestSingleUserHandlers(t *testing.T) {
|
|||
// TODO: check id
|
||||
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)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
|
@ -114,6 +140,85 @@ func TestSingleUserHandlers(t *testing.T) {
|
|||
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)
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
// "path"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ihexxa/gocfg"
|
||||
|
@ -64,3 +70,104 @@ func compareFileContent(fs fspkg.ISimpleFS, uid, filePath string, expectedConten
|
|||
|
||||
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 {
|
||||
ID uint64
|
||||
Name string
|
||||
Pwd string
|
||||
Role string
|
||||
ID uint64 `json:"id,string"`
|
||||
Name string `json:"name"`
|
||||
Pwd string `json:"pwd"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
type IUserStore interface {
|
||||
Init(rootName, rootPwd string) error
|
||||
IsInited() bool
|
||||
AddUser(user *User) error
|
||||
DelUser(id uint64) error
|
||||
GetUser(id uint64) (*User, error)
|
||||
GetUserByName(name string) (*User, error)
|
||||
SetName(id uint64, name string) error
|
||||
SetPwd(id uint64, pwd string) error
|
||||
ListUsers() ([]*User, error)
|
||||
SetRole(id uint64, role string) error
|
||||
AddRole(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)
|
||||
}
|
||||
|
||||
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) {
|
||||
us.mtx.RLock()
|
||||
defer us.mtx.RUnlock()
|
||||
|
@ -258,6 +281,37 @@ func (us *KVUserStore) SetRole(id uint64, role string) error {
|
|||
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 {
|
||||
us.mtx.Lock()
|
||||
defer us.mtx.Unlock()
|
||||
|
|
|
@ -51,6 +51,20 @@ func TestUserStores(t *testing.T) {
|
|||
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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -91,6 +105,22 @@ func TestUserStores(t *testing.T) {
|
|||
if user.Role != role2 {
|
||||
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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue