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:
Hexxa 2021-07-30 21:59:33 -05:00 committed by GitHub
parent 916ec7c2dc
commit aefaca98b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1562 additions and 478 deletions

View file

@ -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).

View file

@ -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
}

View file

@ -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 {

View file

@ -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: {},
});
};
}

View file

@ -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;
@ -44,9 +72,36 @@ export class MockUsersClient {
setPwd = (oldPwd: string, newPwd: string): Promise<Response> => {
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;
}
}

View file

@ -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>

View file

@ -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,

View file

@ -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>()
}
},
};
}

View file

@ -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>
);
}
}

View file

@ -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" }}>

View file

@ -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>

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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}
/>
);
}

View file

@ -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)

View file

@ -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
}

View file

@ -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],
})
}

View file

@ -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)
}

View file

@ -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()
}

View file

@ -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

View file

@ -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)

View 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)
}
}

View file

@ -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)
}
})

View file

@ -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)

View file

@ -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
}

View file

@ -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()

View file

@ -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) {