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

@ -69,21 +69,26 @@
right: 0; right: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.75); background-color: rgba(0, 0, 0, 0.5);
z-index: 100; z-index: 100;
overflow: scroll; overflow: scroll;
} }
#panes .container { #panes .root-container {
max-width: 80rem; max-width: 80rem;
width: 96%; width: 96%;
background-color: white;
z-index: 101; z-index: 101;
text-align: left; text-align: left;
margin: 3rem auto 8rem auto; margin: 3rem auto 8rem auto;
border-radius: 0.6rem; border-radius: 0.6rem;
} }
#panes .container {
background-color: white;
margin: 3rem auto 1rem auto;
border-radius: 0.6rem;
}
#panes .return-btn { #panes .return-btn {
position: fixed; position: fixed;
max-width: 960px; max-width: 960px;
@ -133,12 +138,17 @@
border-top: solid 1px transparent; border-top: solid 1px transparent;
} }
#item-list .dot { .container .dot {
overflow: hidden; overflow: hidden;
margin-left: 1rem; margin-left: 1rem;
margin-right: 1rem; margin-right: 1rem;
} }
#panes .dot {
overflow: hidden;
margin-left: 0;
}
#item-list .vbar { #item-list .vbar {
overflow: hidden; overflow: hidden;
margin: 1.5rem 1rem; margin: 1.5rem 1rem;
@ -164,6 +174,14 @@
display: block; display: block;
} }
.item-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
overflow-wrap: break-word;
display: block;
}
#item-list .item-op { #item-list .item-op {
line-height: 4rem; line-height: 4rem;
} }

View file

@ -130,6 +130,22 @@ func (cl *FilesClient) List(dirPath string) (*http.Response, *fileshdr.ListResp,
return resp, lResp, nil return resp, lResp, nil
} }
func (cl *FilesClient) ListHome() (*http.Response, *fileshdr.ListResp, []error) {
resp, body, errs := cl.r.Get(cl.url("/v1/fs/dirs/home")).
AddCookie(cl.token).
End()
if len(errs) > 0 {
return nil, nil, errs
}
lResp := &fileshdr.ListResp{}
err := json.Unmarshal([]byte(body), lResp)
if err != nil {
return nil, nil, append(errs, err)
}
return resp, lResp, nil
}
func (cl *FilesClient) ListUploadings() (*http.Response, *fileshdr.ListUploadingsResp, []error) { func (cl *FilesClient) ListUploadings() (*http.Response, *fileshdr.ListUploadingsResp, []error) {
resp, body, errs := cl.r.Get(cl.url("/v1/fs/uploadings")). resp, body, errs := cl.r.Get(cl.url("/v1/fs/uploadings")).
AddCookie(cl.token). AddCookie(cl.token).

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/ihexxa/quickshare/src/handlers"
"github.com/ihexxa/quickshare/src/handlers/multiusers" "github.com/ihexxa/quickshare/src/handlers/multiusers"
"github.com/parnurzeal/gorequest" "github.com/parnurzeal/gorequest"
) )
@ -74,6 +75,30 @@ func (cl *SingleUserClient) AddUser(name, pwd, role string, token *http.Cookie)
return resp, auResp, errs return resp, auResp, errs
} }
func (cl *SingleUserClient) DelUser(id string, token *http.Cookie) (*http.Response, string, []error) {
return cl.r.Delete(cl.url("/v1/users/")).
AddCookie(token).
Param(handlers.UserIDParam, id).
End()
}
func (cl *SingleUserClient) ListUsers(token *http.Cookie) (*http.Response, *multiusers.ListUsersResp, []error) {
resp, body, errs := cl.r.Get(cl.url("/v1/users/list")).
AddCookie(token).
End()
if len(errs) > 0 {
return nil, nil, errs
}
lsResp := &multiusers.ListUsersResp{}
err := json.Unmarshal([]byte(body), lsResp)
if err != nil {
errs = append(errs, err)
return nil, nil, errs
}
return resp, lsResp, errs
}
func (cl *SingleUserClient) AddRole(role string, token *http.Cookie) (*http.Response, string, []error) { func (cl *SingleUserClient) AddRole(role string, token *http.Cookie) (*http.Response, string, []error) {
return cl.r.Post(cl.url("/v1/roles/")). return cl.r.Post(cl.url("/v1/roles/")).
AddCookie(token). AddCookie(token).
@ -93,7 +118,7 @@ func (cl *SingleUserClient) DelRole(role string, token *http.Cookie) (*http.Resp
} }
func (cl *SingleUserClient) ListRoles(token *http.Cookie) (*http.Response, *multiusers.ListRolesResp, []error) { func (cl *SingleUserClient) ListRoles(token *http.Cookie) (*http.Response, *multiusers.ListRolesResp, []error) {
resp, body, errs := cl.r.Get(cl.url("/v1/roles/")). resp, body, errs := cl.r.Get(cl.url("/v1/roles/list")).
AddCookie(token). AddCookie(token).
End() End()
if len(errs) > 0 { if len(errs) > 0 {
@ -108,3 +133,20 @@ func (cl *SingleUserClient) ListRoles(token *http.Cookie) (*http.Response, *mult
} }
return resp, lsResp, errs return resp, lsResp, errs
} }
func (cl *SingleUserClient) Self(token *http.Cookie) (*http.Response, *multiusers.SelfResp, []error) {
resp, body, errs := cl.r.Get(cl.url("/v1/users/self")).
AddCookie(token).
End()
if len(errs) > 0 {
return nil, nil, errs
}
selfResp := &multiusers.SelfResp{}
err := json.Unmarshal([]byte(body), selfResp)
if err != nil {
errs = append(errs, err)
return nil, nil, errs
}
return resp, selfResp, errs
}

View file

@ -1,12 +1,21 @@
import axios, { AxiosRequestConfig } from "axios"; import axios, { AxiosRequestConfig } from "axios";
export const defaultTimeout = 10000; export const defaultTimeout = 10000;
export const userIDParam = "uid";
export interface User { export interface User {
ID: string; id: string;
Name: string; name: string;
Pwd: string; pwd: string;
Role: string; role: string;
}
export interface ListUsersResp {
users: Array<User>;
}
export interface ListRolesResp {
roles: Array<string>;
} }
export interface MetadataResp { export interface MetadataResp {
@ -42,7 +51,15 @@ export interface IUsersClient {
login: (user: string, pwd: string) => Promise<Response>; login: (user: string, pwd: string) => Promise<Response>;
logout: () => Promise<Response>; logout: () => Promise<Response>;
isAuthed: () => Promise<Response>; isAuthed: () => Promise<Response>;
self: () => Promise<Response>;
setPwd: (oldPwd: string, newPwd: string) => Promise<Response>; setPwd: (oldPwd: string, newPwd: string) => Promise<Response>;
forceSetPwd: (userID: string, newPwd: string) => Promise<Response>;
addUser: (name: string, pwd: string, role: string) => Promise<Response>;
delUser: (userID: string) => Promise<Response>;
listUsers: () => Promise<Response>;
addRole: (role: string) => Promise<Response>;
delRole: (role: string) => Promise<Response>;
listRoles: () => Promise<Response>;
} }
export interface IFilesClient { export interface IFilesClient {

View file

@ -1,4 +1,4 @@
import { BaseClient, Response } from "./"; import { BaseClient, Response, userIDParam } from "./";
export class UsersClient extends BaseClient { export class UsersClient extends BaseClient {
constructor(url: string) { constructor(url: string) {
@ -16,7 +16,6 @@ export class UsersClient extends BaseClient {
}); });
}; };
// token cookie is set by browser
logout = (): Promise<Response> => { logout = (): Promise<Response> => {
return this.do({ return this.do({
method: "post", method: "post",
@ -31,7 +30,6 @@ export class UsersClient extends BaseClient {
}); });
}; };
// token cookie is set by browser
setPwd = (oldPwd: string, newPwd: string): Promise<Response> => { setPwd = (oldPwd: string, newPwd: string): Promise<Response> => {
return this.do({ return this.do({
method: "patch", method: "patch",
@ -43,8 +41,19 @@ export class UsersClient extends BaseClient {
}); });
}; };
forceSetPwd = (userID: string, newPwd: string): Promise<Response> => {
return this.do({
method: "patch",
url: `${this.url}/v1/users/pwd/force-set`,
data: {
id: userID,
newPwd,
},
});
};
// token cookie is set by browser // token cookie is set by browser
adduser = (name: string, pwd: string, role: string): Promise<Response> => { addUser = (name: string, pwd: string, role: string): Promise<Response> => {
return this.do({ return this.do({
method: "post", method: "post",
url: `${this.url}/v1/users/`, url: `${this.url}/v1/users/`,
@ -55,4 +64,54 @@ export class UsersClient extends BaseClient {
}, },
}); });
}; };
delUser = (userID: string): Promise<Response> => {
return this.do({
method: "delete",
url: `${this.url}/v1/users/`,
params: {
[userIDParam]: userID,
},
});
};
listUsers = (): Promise<Response> => {
return this.do({
method: "get",
url: `${this.url}/v1/users/list`,
params: {},
});
};
addRole = (role: string): Promise<Response> => {
return this.do({
method: "post",
url: `${this.url}/v1/roles/`,
data: { role },
});
};
delRole = (role: string): Promise<Response> => {
return this.do({
method: "delete",
url: `${this.url}/v1/roles/`,
data: { role },
});
};
listRoles = (): Promise<Response> => {
return this.do({
method: "get",
url: `${this.url}/v1/roles/list`,
params: {},
});
};
self = (): Promise<Response> => {
return this.do({
method: "get",
url: `${this.url}/v1/users/self`,
params: {},
});
};
} }

View file

@ -7,7 +7,14 @@ export class MockUsersClient {
private logoutMockResp: Promise<Response>; private logoutMockResp: Promise<Response>;
private isAuthedMockResp: Promise<Response>; private isAuthedMockResp: Promise<Response>;
private setPwdMockResp: Promise<Response>; private setPwdMockResp: Promise<Response>;
private forceSetPwdMockResp: Promise<Response>;
private addUserMockResp: Promise<Response>; private addUserMockResp: Promise<Response>;
private delUserMockResp: Promise<Response>;
private listUsersMockResp: Promise<Response>;
private addRoleMockResp: Promise<Response>;
private delRoleMockResp: Promise<Response>;
private listRolesMockResp: Promise<Response>;
private selfMockResp: Promise<Response>;
constructor(url: string) { constructor(url: string) {
this.url = url; this.url = url;
@ -25,9 +32,30 @@ export class MockUsersClient {
setPwdMock = (resp: Promise<Response>) => { setPwdMock = (resp: Promise<Response>) => {
this.setPwdMockResp = resp; this.setPwdMockResp = resp;
} }
forceSetPwdMock = (resp: Promise<Response>) => {
this.forceSetPwdMockResp = resp;
}
addUserMock = (resp: Promise<Response>) => { addUserMock = (resp: Promise<Response>) => {
this.addUserMockResp = resp; this.addUserMockResp = resp;
} }
delUserMock = (resp: Promise<Response>) => {
this.delUserMockResp = resp;
}
listUsersMock = (resp: Promise<Response>) => {
this.listUsersMockResp = resp;
}
addRoleMock = (resp: Promise<Response>) => {
this.addRoleMockResp = resp;
}
delRoleMock = (resp: Promise<Response>) => {
this.delRoleMockResp = resp;
}
listRolesMock = (resp: Promise<Response>) => {
this.listRolesMockResp = resp;
}
slefMock = (resp: Promise<Response>) => {
this.selfMockResp = resp;
}
login = (user: string, pwd: string): Promise<Response> => { login = (user: string, pwd: string): Promise<Response> => {
return this.loginMockResp; return this.loginMockResp;
@ -45,8 +73,35 @@ export class MockUsersClient {
return this.setPwdMockResp; return this.setPwdMockResp;
} }
forceSetPwd = (userID: string, newPwd: string): Promise<Response> => {
return this.forceSetPwdMockResp;
}
addUser = (name: string, pwd: string, role: string): Promise<Response> => { addUser = (name: string, pwd: string, role: string): Promise<Response> => {
return this.addUserMockResp; return this.addUserMockResp;
} }
delUser = (userID: string): Promise<Response> => {
return this.delUserMockResp;
}
listUsers = (): Promise<Response> => {
return this.listUsersMockResp;
}
addRole = (role: string): Promise<Response> => {
return this.addRoleMockResp;
}
delRole = (role: string): Promise<Response> => {
return this.delRoleMockResp;
}
listRoles = (): Promise<Response> => {
return this.listRolesMockResp;
}
self = (): Promise<Response> => {
return this.selfMockResp;
}
} }

View file

@ -259,52 +259,6 @@ export class Browser extends React.Component<Props, State, {}> {
const sizeCellClass = this.props.isVertical ? `hidden margin-s` : ``; const sizeCellClass = this.props.isVertical ? `hidden margin-s` : ``;
const modTimeCellClass = this.props.isVertical ? `hidden margin-s` : ``; const modTimeCellClass = this.props.isVertical ? `hidden margin-s` : ``;
const layoutChildren = [
<button
type="button"
onClick={() => this.delete()}
className="red0-bg white-font margin-t-m margin-b-m"
>
Delete Selected
</button>,
<button
type="button"
onClick={() => this.moveHere()}
className="grey1-bg white-font margin-t-m margin-b-m"
>
Paste
</button>,
<span className="inline-block margin-t-m margin-b-m">
<input
type="text"
onChange={this.onInputChange}
value={this.state.inputValue}
className="black0-font margin-r-m"
placeholder="folder name"
/>
<button onClick={this.onMkDir} className="grey1-bg white-font">
Create Folder
</button>
</span>,
<span className="inline-block margin-t-m margin-b-m">
<button onClick={this.onClickUpload} className="green0-bg white-font">
Upload Files
</button>
<input
type="file"
onChange={this.addUploads}
multiple={true}
value={this.props.uploadValue}
ref={this.assignInput}
className="black0-font hidden"
/>
</span>,
];
// const ops = (
// <Layouter isHorizontal={false} elements={layoutChildren}></Layouter>
// );
const ops = ( const ops = (
<div> <div>
<div> <div>

View file

@ -124,17 +124,6 @@ export class Updater {
: this.props.items; : this.props.items;
}; };
goHome = async (): Promise<void> => {
const listResp = await this.filesClient.listHome();
// how to get current dir? to dirPath?
// this.props.dirPath = dirParts;
this.props.items =
listResp.status === 200
? List<MetadataResp>(listResp.data.metadatas)
: this.props.items;
};
moveHere = async ( moveHere = async (
srcDir: string, srcDir: string,
dstDir: string, dstDir: string,

View file

@ -60,16 +60,17 @@ export function initState(): ICoreState {
uploadFiles: List<File>([]), uploadFiles: List<File>([]),
}, },
panes: { panes: {
userRole: "",
displaying: "", displaying: "",
paneNames: Set<string>(["settings", "login"]), paneNames: Set<string>(["settings", "login", "admin"]),
login: { login: {
authed: false, authed: false,
}, },
},
admin: { admin: {
users: Map<string, User>(), users: Map<string, User>(),
roles: Set<string>() roles: Set<string>(),
} },
},
}, },
}; };
} }
@ -92,16 +93,17 @@ export function mockState(): ICoreState {
uploadFiles: List<File>([]), uploadFiles: List<File>([]),
}, },
panes: { panes: {
userRole: "",
displaying: "", displaying: "",
paneNames: Set<string>(["settings", "login"]), paneNames: Set<string>(["settings", "login", "admin"]),
login: { login: {
authed: false, authed: false,
}, },
},
admin: { admin: {
users: Map<string, User>(), users: Map<string, User>(),
roles: Set<string>() roles: Set<string>(),
} },
},
}, },
}; };
} }

View file

@ -2,11 +2,8 @@ import * as React from "react";
import { Map, Set } from "immutable"; import { Map, Set } from "immutable";
import { ICoreState } from "./core_state"; import { ICoreState } from "./core_state";
import { IUsersClient, User} from "../client"; import { User } from "../client";
import { UsersClient } from "../client/users";
import { Updater as PanesUpdater } from "./panes"; import { Updater as PanesUpdater } from "./panes";
import { updater as BrowserUpdater } from "./browser.updater";
import { Layouter } from "./layouter";
export interface Props { export interface Props {
users: Map<string, User>; users: Map<string, User>;
@ -14,165 +11,408 @@ export interface Props {
update?: (updater: (prevState: ICoreState) => ICoreState) => void; update?: (updater: (prevState: ICoreState) => ICoreState) => void;
} }
export class Updater { export interface UserFormProps {
private static props: Props; key: string;
private static client: IUsersClient; id: string;
name: string;
role: string;
roles: Set<string>;
update?: (updater: (prevState: ICoreState) => ICoreState) => void;
}
static init = (props: Props) => (Updater.props = { ...props }); export interface UserFormState {
id: string;
name: string;
newPwd1: string;
newPwd2: string;
role: string;
}
static setClient = (client: IUsersClient): void => { export class UserForm extends React.Component<
Updater.client = client; UserFormProps,
}; UserFormState,
{}
// static adduser = async (user: User): Promise<boolean> => { > {
// const resp = await Updater.client.add constructor(p: UserFormProps) {
// } super(p);
this.state = {
// static login = async (user: string, pwd: string): Promise<boolean> => { id: p.id,
// const resp = await Updater.client.login(user, pwd); name: p.name,
// Updater.setAuthed(resp.status === 200); newPwd1: "",
// return resp.status === 200; newPwd2: "",
// }; role: p.role,
// static logout = async (): Promise<boolean> => {
// const resp = await Updater.client.logout();
// Updater.setAuthed(false);
// return resp.status === 200;
// };
// static isAuthed = async (): Promise<boolean> => {
// const resp = await Updater.client.isAuthed();
// return resp.status === 200;
// };
// static initIsAuthed = async (): Promise<void> => {
// return Updater.isAuthed().then((isAuthed) => {
// Updater.setAuthed(isAuthed);
// });
// };
static setState = (preState: ICoreState): ICoreState => {
preState.panel.authPane = {
...preState.panel.authPane,
...Updater.props,
};
return preState;
}; };
} }
// export interface State { changePwd1 = (ev: React.ChangeEvent<HTMLInputElement>) => {
// user: string; this.setState({ newPwd1: ev.target.value });
// pwd: string; };
// } changePwd2 = (ev: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ newPwd2: ev.target.value });
};
changeRole = (ev: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ role: ev.target.value });
};
// export class AuthPane extends React.Component<Props, State, {}> { setPwd = () => {
// private update: (updater: (prevState: ICoreState) => ICoreState) => void; if (this.state.newPwd1 !== this.state.newPwd2) {
// constructor(p: Props) { alert("2 passwords do not match, please check.");
// super(p); return;
// Updater.init(p); }
// Updater.setClient(new UsersClient(""));
// this.update = p.update;
// this.state = {
// user: "",
// pwd: "",
// };
// this.initIsAuthed(); PanesUpdater.forceSetPwd(this.state.id, this.state.newPwd1).then(
// } (ok: boolean) => {
if (ok) {
alert("password is updated");
} else {
alert("failed to update password");
}
this.setState({
newPwd1: "",
newPwd2: "",
});
}
);
};
// changeUser = (ev: React.ChangeEvent<HTMLInputElement>) => { delUser = () => {
// this.setState({ user: ev.target.value }); PanesUpdater.delUser(this.state.id)
// }; .then((ok: boolean) => {
if (!ok) {
alert("failed to delete user");
}
return PanesUpdater.listUsers();
})
.then((_: boolean) => {
this.props.update(PanesUpdater.updateState);
});
};
// changePwd = (ev: React.ChangeEvent<HTMLInputElement>) => { // setRole = () => {};
// this.setState({ pwd: ev.target.value });
// };
// initIsAuthed = () => { render() {
// Updater.initIsAuthed().then(() => { return (
// this.update(Updater.setAuthPane); <span>
// }); <span className="flex-list-container">
// }; <div className="flex-list-item-l">
<span className="vbar green0-bg"></span>
<div
className="margin-l-m"
style={{
flexDirection: "column",
}}
>
<div className="bold item-name">Name: {this.props.name}</div>
<div className="grey1-font item-name">
ID: {this.props.id} / Role: {this.props.role}
</div>
</div>
</div>
<div
className="flex-list-item-r"
style={{
flexDirection: "column",
flexBasis: "80%",
alignItems: "flex-end",
}}
>
<div className="margin-t-m">
<button
onClick={this.delUser}
className="grey1-bg white-font margin-r-m"
>
Delete User
</button>
</div>
// login = () => { {/* no API yet */}
// Updater.login(this.state.user, this.state.pwd) {/* <div className="margin-t-m">
// .then((ok: boolean) => { <input
// if (ok) { name={`${this.props.id}-role`}
// this.update(Updater.setAuthPane); type="text"
// this.setState({ user: "", pwd: "" }); onChange={this.changeRole}
// // close all the panes value={this.state.role}
// PanesUpdater.displayPane(""); className="black0-font margin-r-m"
// this.update(PanesUpdater.updateState); placeholder={this.props.role}
/>
<button
onClick={this.setRole}
className="grey1-bg white-font margin-r-m"
>
Update Role
</button>
</div> */}
</div>
</span>
// // refresh <div className="margin-t-m">
// return BrowserUpdater().setHomeItems(); <input
// } else { name={`${this.props.id}-pwd1`}
// this.setState({ user: "", pwd: "" }); type="password"
// alert("Failed to login."); onChange={this.changePwd1}
// } value={this.state.newPwd1}
// }) className="black0-font margin-r-m"
// .then(() => { placeholder="new password"
// return BrowserUpdater().refreshUploadings(); />
// }) <input
// .then((_: boolean) => { name={`${this.props.id}-pwd2`}
// this.update(BrowserUpdater().setBrowser); type="password"
// }); onChange={this.changePwd2}
// }; value={this.state.newPwd2}
className="black0-font margin-r-m"
placeholder="repeat password"
/>
<button
onClick={this.setPwd}
className="grey1-bg white-font margin-r-m"
>
Update
</button>
</div>
// logout = () => { </span>
// Updater.logout().then((ok: boolean) => { );
// if (ok) { }
// this.update(Updater.setAuthPane); }
// } else {
// alert("Failed to logout.");
// }
// });
// };
// render() { export interface State {
// const elements: Array<JSX.Element> = [ newUserName: string;
// <input newUserPwd1: string;
// name="user" newUserPwd2: string;
// type="text" newUserRole: string;
// onChange={this.changeUser} newRole: string;
// value={this.state.user} }
// className="black0-font margin-t-m margin-b-m" export class AdminPane extends React.Component<Props, State, {}> {
// // style={{ width: "80%" }} constructor(p: Props) {
// placeholder="user name" super(p);
// />, this.state = {
// <input newUserName: "",
// name="pwd" newUserPwd1: "",
// type="password" newUserPwd2: "",
// onChange={this.changePwd} newUserRole: "",
// value={this.state.pwd} newRole: "",
// className="black0-font margin-t-m margin-b-m" };
// // style={{ width: "80%" }} }
// placeholder="password"
// />,
// <button
// onClick={this.login}
// className="green0-bg white-font margin-t-m margin-b-m"
// >
// Log in
// </button>,
// ];
// return ( onChangeUserName = (ev: React.ChangeEvent<HTMLInputElement>) => {
// <span> this.setState({ newUserName: ev.target.value });
// <div };
// className="margin-l-l" onChangeUserPwd1 = (ev: React.ChangeEvent<HTMLInputElement>) => {
// style={{ display: this.props.authed ? "none" : "block" }} this.setState({ newUserPwd1: ev.target.value });
// > };
// {/* <h5 className="black-font">Login</h5> */} onChangeUserPwd2 = (ev: React.ChangeEvent<HTMLInputElement>) => {
// <Layouter isHorizontal={false} elements={elements} /> this.setState({ newUserPwd2: ev.target.value });
// </div> };
onChangeUserRole = (ev: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ newUserRole: ev.target.value });
};
onChangeRole = (ev: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ newRole: ev.target.value });
};
// <span style={{ display: this.props.authed ? "inherit" : "none" }}> addRole = () => {
// <button onClick={this.logout} className="grey1-bg white-font"> PanesUpdater.addRole(this.state.newRole)
// Log out .then((ok: boolean) => {
// </button> if (!ok) {
// </span> alert("failed to add role");
// </span> }
// ); return PanesUpdater.listRoles();
// } })
// } .then(() => {
this.props.update(PanesUpdater.updateState);
});
};
delRole = (role: string) => {
if (
!confirm(
"After deleting this role, some of users may not be able to login."
)
) {
return;
}
PanesUpdater.delRole(role)
.then((ok: boolean) => {
if (!ok) {
alert("failed to delete role");
}
return PanesUpdater.listRoles();
})
.then(() => {
this.props.update(PanesUpdater.updateState);
});
};
addUser = () => {
if (this.state.newUserPwd1 !== this.state.newUserPwd2) {
alert("2 passwords do not match, please check.");
return;
}
PanesUpdater.addUser({
id: "", // backend will fill it
name: this.state.newUserName,
pwd: this.state.newUserPwd1,
role: this.state.newUserRole,
})
.then((ok: boolean) => {
if (!ok) {
alert("failed to add user");
}
this.setState({
newUserName: "",
newUserPwd1: "",
newUserPwd2: "",
newUserRole: "",
});
return PanesUpdater.listUsers();
})
.then(() => {
this.props.update(PanesUpdater.updateState);
});
};
render() {
const userList = this.props.users.valueSeq().map((user: User) => {
return (
<div className="margin-t-m">
<UserForm
key={user.id}
id={user.id}
name={user.name}
role={user.role}
roles={this.props.roles}
update={this.props.update}
/>
</div>
);
});
const roleList = this.props.roles.valueSeq().map((role: string) => {
return (
<div key={role} className="flex-list-container margin-b-m">
<div className="flex-list-item-l">
<span className="dot red0-bg"></span>
<span className="bold">{role}</span>
</div>
<div className="flex-list-item-r">
<button
onClick={() => {
this.delRole(role);
}}
className="grey1-bg white-font margin-r-m"
>
Delete
</button>
</div>
</div>
);
});
return (
<div className="font-size-m">
<div className="container">
<div className="flex-list-container padding-l">
{/* <span className="inline-block margin-t-m margin-b-m"> */}
<div
className="flex-list-item-l"
style={{
flexDirection: "column",
alignItems: "flex-start",
}}
>
<input
type="text"
onChange={this.onChangeUserName}
value={this.state.newUserName}
className="black0-font margin-b-m"
placeholder="new user name"
/>
<input
type="text"
onChange={this.onChangeUserRole}
value={this.state.newUserRole}
className="black0-font margin-b-m"
placeholder="new user role"
/>
<input
type="password"
onChange={this.onChangeUserPwd1}
value={this.state.newUserPwd1}
className="black0-font margin-b-m"
placeholder="password"
/>
<input
type="password"
onChange={this.onChangeUserPwd2}
value={this.state.newUserPwd2}
className="black0-font margin-b-m"
placeholder="repeat password"
/>
</div>
<div className="flex-list-item-r">
<button
onClick={this.addUser}
className="grey1-bg white-font margin-r-m"
>
Create User
</button>
</div>
{/* </span> */}
</div>
</div>
<div className="container">
<div className="padding-l">
<div className="flex-list-container bold">
<span className="flex-list-item-l">
<span className="dot black-bg"></span>
<span>Users</span>
</span>
<span className="flex-list-item-r padding-r-m"></span>
</div>
{userList}
</div>
</div>
<div className="container">
<div className="flex-list-container padding-l">
<div className="flex-list-item-l">
<span className="inline-block margin-t-m margin-b-m">
<input
type="text"
onChange={this.onChangeRole}
value={this.state.newRole}
className="black0-font margin-r-m"
placeholder="new role name"
/>
</span>
</div>
<div className="flex-list-item-r">
<button
onClick={this.addRole}
className="grey1-bg white-font margin-r-m"
>
Create Role
</button>
</div>
</div>
</div>
<div className="container">
<div className="padding-l">
<div className="flex-list-container bold margin-b-m">
<span className="flex-list-item-l">
<span className="dot black-bg"></span>
<span>Roles</span>
</span>
<span className="flex-list-item-r padding-r-m"></span>
</div>
{roleList}
</div>
</div>
</div>
);
}
}

View file

@ -159,12 +159,14 @@ export class AuthPane extends React.Component<Props, State, {}> {
return ( return (
<span> <span>
<div <div
className="margin-l-l" className="container"
style={{ display: this.props.authed ? "none" : "block" }} style={{ display: this.props.authed ? "none" : "block" }}
> >
<div className="padding-l">
{/* <h5 className="black-font">Login</h5> */} {/* <h5 className="black-font">Login</h5> */}
<Layouter isHorizontal={false} elements={elements} /> <Layouter isHorizontal={false} elements={elements} />
</div> </div>
</div>
<span style={{ display: this.props.authed ? "inherit" : "none" }}> <span style={{ display: this.props.authed ? "inherit" : "none" }}>
<button onClick={this.logout} className="grey1-bg white-font"> <button onClick={this.logout} className="grey1-bg white-font">

View file

@ -120,6 +120,7 @@ export class PaneSettings extends React.Component<Props, State, {}> {
]; ];
return ( return (
<div className="container">
<div className="padding-l"> <div className="padding-l">
<div> <div>
<div className="flex-list-container"> <div className="flex-list-container">
@ -171,7 +172,11 @@ export class PaneSettings extends React.Component<Props, State, {}> {
<h5 className="black-font">Logout</h5> <h5 className="black-font">Logout</h5>
</div> </div>
<div className="flex-list-item-r"> <div className="flex-list-item-r">
<AuthPane authed={this.props.login.authed} update={this.update} /> <AuthPane
authed={this.props.login.authed}
update={this.update}
/>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,21 +1,30 @@
import * as React from "react"; import * as React from "react";
import { Set, Map } from "immutable"; import { Set, Map } from "immutable";
import { IUsersClient, User, ListUsersResp, ListRolesResp } from "../client";
import { UsersClient } from "../client/users";
import { ICoreState } from "./core_state"; import { ICoreState } from "./core_state";
import { PaneSettings } from "./pane_settings"; import { PaneSettings } from "./pane_settings";
import { AdminPane, Props as AdminPaneProps } from "./pane_admin";
import { AuthPane, Props as AuthPaneProps } from "./pane_login"; import { AuthPane, Props as AuthPaneProps } from "./pane_login";
export interface Props { export interface Props {
userRole: string;
displaying: string; displaying: string;
paneNames: Set<string>; paneNames: Set<string>;
login: AuthPaneProps; login: AuthPaneProps;
admin: AdminPaneProps;
update?: (updater: (prevState: ICoreState) => ICoreState) => void; update?: (updater: (prevState: ICoreState) => ICoreState) => void;
} }
export class Updater { export class Updater {
static props: Props; static props: Props;
private static client: IUsersClient;
static init = (props: Props) => (Updater.props = { ...props }); static init = (props: Props) => (Updater.props = { ...props });
static setClient = (client: IUsersClient): void => {
Updater.client = client;
};
static displayPane = (paneName: string) => { static displayPane = (paneName: string) => {
if (paneName === "") { if (paneName === "") {
@ -31,7 +40,85 @@ export class Updater {
} }
}; };
static self = async (): Promise<boolean> => {
const resp = await Updater.client.self();
if (resp.status === 200) {
Updater.props.userRole = resp.data.role;
return true;
}
return false;
}
static addUser = async (user: User): Promise<boolean> => {
const resp = await Updater.client.addUser(user.name, user.pwd, user.role);
// TODO: should return uid instead
return resp.status === 200;
};
static delUser = async (userID: string): Promise<boolean> => {
const resp = await Updater.client.delUser(userID);
return resp.status === 200;
};
static setRole = async (userID: string, role: string): Promise<boolean> => {
const resp = await Updater.client.delUser(userID);
return resp.status === 200;
};
static forceSetPwd = async (
userID: string,
pwd: string
): Promise<boolean> => {
const resp = await Updater.client.forceSetPwd(userID, pwd);
return resp.status === 200;
};
static listUsers = async (): Promise<boolean> => {
const resp = await Updater.client.listUsers();
if (resp.status !== 200) {
return false;
}
const lsRes = resp.data as ListUsersResp;
let users = Map<User>({});
lsRes.users.forEach((user: User) => {
users = users.set(user.name, user);
});
Updater.props.admin.users = users;
return true;
};
static addRole = async (role: string): Promise<boolean> => {
const resp = await Updater.client.addRole(role);
// TODO: should return uid instead
return resp.status === 200;
};
static delRole = async (role: string): Promise<boolean> => {
const resp = await Updater.client.delRole(role);
return resp.status === 200;
};
static listRoles = async (): Promise<boolean> => {
const resp = await Updater.client.listRoles();
if (resp.status !== 200) {
return false;
}
const lsRes = resp.data as ListRolesResp;
let roles = Set<string>();
Object.keys(lsRes.roles).forEach((role: string) => {
roles = roles.add(role);
});
Updater.props.admin.roles = roles;
return true;
};
static updateState = (prevState: ICoreState): ICoreState => { static updateState = (prevState: ICoreState): ICoreState => {
console.log(prevState, Updater.props);
return { return {
...prevState, ...prevState,
panel: { panel: {
@ -47,6 +134,7 @@ export class Panes extends React.Component<Props, State, {}> {
constructor(p: Props) { constructor(p: Props) {
super(p); super(p);
Updater.init(p); Updater.init(p);
Updater.setClient(new UsersClient(""));
} }
closePane = () => { closePane = () => {
@ -63,7 +151,7 @@ export class Panes extends React.Component<Props, State, {}> {
displaying = "login"; displaying = "login";
} }
const panesMap: Map<string, JSX.Element> = Map({ let panesMap: Map<string, JSX.Element> = Map({
settings: ( settings: (
<PaneSettings login={this.props.login} update={this.props.update} /> <PaneSettings login={this.props.login} update={this.props.update} />
), ),
@ -72,6 +160,17 @@ export class Panes extends React.Component<Props, State, {}> {
), ),
}); });
if (this.props.userRole === "admin") {
panesMap = panesMap.set(
"admin",
<AdminPane
users={this.props.admin.users}
roles={this.props.admin.roles}
update={this.props.update}
/>
);
}
const panes = panesMap.keySeq().map((paneName: string): JSX.Element => { const panes = panesMap.keySeq().map((paneName: string): JSX.Element => {
const isDisplay = displaying === paneName ? "" : "hidden"; const isDisplay = displaying === paneName ? "" : "hidden";
return ( return (
@ -84,24 +183,25 @@ export class Panes extends React.Component<Props, State, {}> {
const btnClass = displaying === "login" ? "hidden" : ""; const btnClass = displaying === "login" ? "hidden" : "";
return ( return (
<div id="panes" className={displaying === "" ? "hidden" : ""}> <div id="panes" className={displaying === "" ? "hidden" : ""}>
<div className="root-container">
<div className="container"> <div className="container">
<div className="flex-list-container padding-l"> <div className="flex-list-container padding-l">
<h3 className="flex-list-item-l txt-cap">{displaying}</h3> <h3 className="flex-list-item-l txt-cap">{displaying}</h3>
<div className="flex-list-item-r"> <div className="flex-list-item-r">
<button <button
onClick={this.closePane} onClick={this.closePane}
className={`black0-bg white-font ${btnClass}`} className={`red0-bg white-font ${btnClass}`}
> >
Close Close
</button> </button>
</div> </div>
</div> </div>
<div className="hr white0-bg margin-b-m margin-l-m margin-r-m"></div>
{panes}
<div className="padding-l"></div>
</div> </div>
{panes}
</div>
{/* <div className="hr white0-bg margin-b-m margin-l-m margin-r-m"></div> */}
{/* <div className="padding-l"></div> */}
</div> </div>
); );
} }

View file

@ -11,7 +11,6 @@ export interface Props {
browser: BrowserProps; browser: BrowserProps;
authPane: PaneLoginProps; authPane: PaneLoginProps;
panes: PanesProps; panes: PanesProps;
admin: PaneAdminProps;
update?: (updater: (prevState: ICoreState) => ICoreState) => void; update?: (updater: (prevState: ICoreState) => ICoreState) => void;
} }
@ -38,15 +37,22 @@ export class RootFrame extends React.Component<Props, State, {}> {
this.props.update(PanesUpdater.updateState); this.props.update(PanesUpdater.updateState);
}; };
showAdmin = () => {
PanesUpdater.displayPane("admin");
this.props.update(PanesUpdater.updateState);
};
render() { render() {
const update = this.props.update; const update = this.props.update;
return ( return (
<div className="theme-white desktop"> <div className="theme-white desktop">
<div id="bg" className="bg bg-img font-m"> <div id="bg" className="bg bg-img font-m">
<Panes <Panes
userRole={this.props.panes.userRole}
displaying={this.props.panes.displaying} displaying={this.props.panes.displaying}
paneNames={this.props.panes.paneNames} paneNames={this.props.panes.paneNames}
login={this.props.authPane} login={this.props.authPane}
admin={this.props.panes.admin}
update={update} update={update}
/> />
@ -68,6 +74,12 @@ export class RootFrame extends React.Component<Props, State, {}> {
> >
Settings Settings
</button> </button>
<button
onClick={this.showAdmin}
className="grey1-bg white-font margin-r-m"
>
Admin
</button>
</span> </span>
</div> </div>
</div> </div>

View file

@ -1,6 +1,7 @@
import * as React from "react"; import * as React from "react";
import { updater as BrowserUpdater } from "./browser.updater"; import { updater as BrowserUpdater } from "./browser.updater";
import { Updater as PanesUpdater } from "./panes";
import { ICoreState, init } from "./core_state"; import { ICoreState, init } from "./core_state";
import { RootFrame } from "./root_frame"; import { RootFrame } from "./root_frame";
import { FilesClient } from "../client/files"; import { FilesClient } from "../client/files";
@ -26,6 +27,19 @@ export class StateMgr extends React.Component<Props, State, {}> {
}) })
.then((_: boolean) => { .then((_: boolean) => {
this.update(BrowserUpdater().setBrowser); this.update(BrowserUpdater().setBrowser);
})
.then(() => {
return PanesUpdater.self();
})
.then(() => {
return PanesUpdater.listRoles();
})
.then((_: boolean) => {
return PanesUpdater.listUsers();
})
.then((_: boolean) => {
console.log(PanesUpdater);
this.update(PanesUpdater.updateState);
}); });
}; };
@ -41,7 +55,6 @@ export class StateMgr extends React.Component<Props, State, {}> {
update={this.update} update={this.update}
browser={this.state.panel.browser} browser={this.state.panel.browser}
panes={this.state.panel.panes} panes={this.state.panel.panes}
admin={this.state.panel.admin}
/> />
); );
} }

View file

@ -118,7 +118,7 @@ func (h *FileHandlers) Create(c *gin.Context) {
return return
} }
tmpFilePath := q.GetTmpPath(userID, req.Path) tmpFilePath := q.UploadPath(userID, req.Path)
locker := h.NewAutoLocker(c, lockName(tmpFilePath)) locker := h.NewAutoLocker(c, lockName(tmpFilePath))
locker.Exec(func() { locker.Exec(func() {
err := h.deps.FS().Create(tmpFilePath) err := h.deps.FS().Create(tmpFilePath)
@ -295,7 +295,7 @@ func (h *FileHandlers) UploadChunk(c *gin.Context) {
return return
} }
tmpFilePath := q.GetTmpPath(userID, req.Path) tmpFilePath := q.UploadPath(userID, req.Path)
locker := h.NewAutoLocker(c, lockName(tmpFilePath)) locker := h.NewAutoLocker(c, lockName(tmpFilePath))
locker.Exec(func() { locker.Exec(func() {
var err error var err error
@ -407,7 +407,7 @@ func (h *FileHandlers) UploadStatus(c *gin.Context) {
return return
} }
tmpFilePath := q.GetTmpPath(userID, filePath) tmpFilePath := q.UploadPath(userID, filePath)
locker := h.NewAutoLocker(c, lockName(tmpFilePath)) locker := h.NewAutoLocker(c, lockName(tmpFilePath))
locker.Exec(func() { locker.Exec(func() {
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(userID, tmpFilePath) _, fileSize, uploaded, err := h.uploadMgr.GetInfo(userID, tmpFilePath)
@ -549,7 +549,8 @@ func (h *FileHandlers) List(c *gin.Context) {
func (h *FileHandlers) ListHome(c *gin.Context) { func (h *FileHandlers) ListHome(c *gin.Context) {
userID := c.MustGet(q.UserIDParam).(string) userID := c.MustGet(q.UserIDParam).(string)
infos, err := h.deps.FS().ListDir(userID) fsPath := q.FsRootPath(userID, "/")
infos, err := h.deps.FS().ListDir(fsPath)
if err != nil { if err != nil {
c.JSON(q.ErrResp(c, 500, err)) c.JSON(q.ErrResp(c, 500, err))
return return
@ -565,7 +566,7 @@ func (h *FileHandlers) ListHome(c *gin.Context) {
} }
c.JSON(200, &ListResp{ c.JSON(200, &ListResp{
Cwd: userID, Cwd: fsPath,
Metadatas: metadatas, Metadatas: metadatas,
}) })
} }
@ -606,7 +607,7 @@ func (h *FileHandlers) DelUploading(c *gin.Context) {
userID := c.MustGet(q.UserIDParam).(string) userID := c.MustGet(q.UserIDParam).(string)
var err error var err error
tmpFilePath := q.GetTmpPath(userID, filePath) tmpFilePath := q.UploadPath(userID, filePath)
locker := h.NewAutoLocker(c, lockName(tmpFilePath)) locker := h.NewAutoLocker(c, lockName(tmpFilePath))
locker.Exec(func() { locker.Exec(func() {
err = h.deps.FS().Remove(tmpFilePath) err = h.deps.FS().Remove(tmpFilePath)

View file

@ -102,7 +102,12 @@ func (um *UploadMgr) DelInfo(user, filePath string) error {
} }
func (um *UploadMgr) ListInfo(user string) ([]*UploadInfo, error) { func (um *UploadMgr) ListInfo(user string) ([]*UploadInfo, error) {
infoMap, err := um.kv.ListStringsIn(UploadNS(user)) ns := UploadNS(user)
if !um.kv.HasNamespace(ns) {
return nil, nil
}
infoMap, err := um.kv.ListStringsIn(ns)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -39,10 +39,14 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error)
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/logout"): true, apiRuleCname(userstore.AdminRole, "POST", "/v1/users/logout"): true,
apiRuleCname(userstore.AdminRole, "GET", "/v1/users/isauthed"): true, apiRuleCname(userstore.AdminRole, "GET", "/v1/users/isauthed"): true,
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/users/pwd"): true, apiRuleCname(userstore.AdminRole, "PATCH", "/v1/users/pwd"): true,
apiRuleCname(userstore.AdminRole, "PATCH", "/v1/users/pwd/force-set"): true,
apiRuleCname(userstore.AdminRole, "POST", "/v1/users/"): true, apiRuleCname(userstore.AdminRole, "POST", "/v1/users/"): true,
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/users/"): true,
apiRuleCname(userstore.AdminRole, "GET", "/v1/users/list"): true,
apiRuleCname(userstore.AdminRole, "GET", "/v1/users/self"): true,
apiRuleCname(userstore.AdminRole, "POST", "/v1/roles/"): true, apiRuleCname(userstore.AdminRole, "POST", "/v1/roles/"): true,
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/roles/"): true, apiRuleCname(userstore.AdminRole, "DELETE", "/v1/roles/"): true,
apiRuleCname(userstore.AdminRole, "GET", "/v1/roles/"): true, apiRuleCname(userstore.AdminRole, "GET", "/v1/roles/list"): true,
apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/files"): true, apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/files"): true,
apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/files"): true, apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/files"): true,
apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files"): true, apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files"): true,
@ -63,6 +67,7 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error)
apiRuleCname(userstore.UserRole, "POST", "/v1/users/logout"): true, apiRuleCname(userstore.UserRole, "POST", "/v1/users/logout"): true,
apiRuleCname(userstore.UserRole, "GET", "/v1/users/isauthed"): true, apiRuleCname(userstore.UserRole, "GET", "/v1/users/isauthed"): true,
apiRuleCname(userstore.UserRole, "PATCH", "/v1/users/pwd"): true, apiRuleCname(userstore.UserRole, "PATCH", "/v1/users/pwd"): true,
apiRuleCname(userstore.UserRole, "GET", "/v1/users/self"): true,
apiRuleCname(userstore.UserRole, "POST", "/v1/fs/files"): true, apiRuleCname(userstore.UserRole, "POST", "/v1/fs/files"): true,
apiRuleCname(userstore.UserRole, "DELETE", "/v1/fs/files"): true, apiRuleCname(userstore.UserRole, "DELETE", "/v1/fs/files"): true,
apiRuleCname(userstore.UserRole, "GET", "/v1/fs/files"): true, apiRuleCname(userstore.UserRole, "GET", "/v1/fs/files"): true,
@ -82,6 +87,7 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error)
apiRuleCname(userstore.VisitorRole, "GET", publicPath): true, apiRuleCname(userstore.VisitorRole, "GET", publicPath): true,
apiRuleCname(userstore.VisitorRole, "POST", "/v1/users/login"): true, apiRuleCname(userstore.VisitorRole, "POST", "/v1/users/login"): true,
apiRuleCname(userstore.VisitorRole, "GET", "/v1/users/isauthed"): true, apiRuleCname(userstore.VisitorRole, "GET", "/v1/users/isauthed"): true,
apiRuleCname(userstore.VisitorRole, "GET", "/v1/users/self"): true,
apiRuleCname(userstore.VisitorRole, "GET", "/v1/fs/files"): true, apiRuleCname(userstore.VisitorRole, "GET", "/v1/fs/files"): true,
apiRuleCname(userstore.VisitorRole, "OPTIONS", "/v1/settings/health"): true, apiRuleCname(userstore.VisitorRole, "OPTIONS", "/v1/settings/health"): true,
} }
@ -97,12 +103,12 @@ func (h *MultiUsersSvc) Init(adminName, adminPwd string) (string, error) {
var err error var err error
userID := "0" userID := "0"
fsPath := q.HomePath(userID, "/") fsPath := q.FsRootPath(userID, "/")
if err = h.deps.FS().MkdirAll(fsPath); err != nil { if err = h.deps.FS().MkdirAll(fsPath); err != nil {
return "", err return "", err
} }
uploadingsPath := q.GetTmpPath(userID, "/") uploadFolder := q.UploadFolder(userID)
if err = h.deps.FS().MkdirAll(uploadingsPath); err != nil { if err = h.deps.FS().MkdirAll(uploadFolder); err != nil {
return "", err return "", err
} }
@ -231,6 +237,57 @@ func (h *MultiUsersSvc) SetPwd(c *gin.Context) {
c.JSON(q.Resp(200)) c.JSON(q.Resp(200))
} }
type ForceSetPwdReq struct {
ID string `json:"id"`
NewPwd string `json:"newPwd"`
}
func (h *MultiUsersSvc) ForceSetPwd(c *gin.Context) {
req := &ForceSetPwdReq{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(q.ErrResp(c, 400, err))
return
}
claims, err := h.getUserInfo(c)
if err != nil {
c.JSON(q.ErrResp(c, 401, err))
return
}
if claims[q.RoleParam] != userstore.AdminRole {
c.JSON(q.ErrResp(c, 403, errors.New("operation denied")))
return
}
targetUID, err := strconv.ParseUint(req.ID, 10, 64)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
targetUser, err := h.deps.Users().GetUser(targetUID)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
if targetUser.Role == userstore.AdminRole {
c.JSON(q.ErrResp(c, 403, errors.New("can not set admin's password")))
return
}
newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPwd), 10)
if err != nil {
c.JSON(q.ErrResp(c, 500, errors.New("fail to set password")))
return
}
err = h.deps.Users().SetPwd(targetUser.ID, string(newHash))
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
c.JSON(q.Resp(200))
}
type AddUserReq struct { type AddUserReq struct {
Name string `json:"name"` Name string `json:"name"`
Pwd string `json:"pwd"` Pwd string `json:"pwd"`
@ -267,14 +324,14 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) {
// TODO: following operations must be atomic // TODO: following operations must be atomic
// TODO: check if the folders already exists // TODO: check if the folders already exists
userID := c.MustGet(q.UserIDParam).(string) uidStr := fmt.Sprint(uid)
homePath := q.HomePath(userID, "/") fsRootFolder := q.FsRootPath(uidStr, "/")
if err = h.deps.FS().MkdirAll(homePath); err != nil { if err = h.deps.FS().MkdirAll(fsRootFolder); err != nil {
c.JSON(q.ErrResp(c, 500, err)) c.JSON(q.ErrResp(c, 500, err))
return return
} }
uploadingsPath := q.GetTmpPath(userID, "/") uploadFolder := q.UploadFolder(uidStr)
if err = h.deps.FS().MkdirAll(uploadingsPath); err != nil { if err = h.deps.FS().MkdirAll(uploadFolder); err != nil {
c.JSON(q.ErrResp(c, 500, err)) c.JSON(q.ErrResp(c, 500, err))
return return
} }
@ -293,6 +350,71 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) {
c.JSON(200, &AddUserResp{ID: fmt.Sprint(uid)}) c.JSON(200, &AddUserResp{ID: fmt.Sprint(uid)})
} }
type DelUserResp struct {
ID string `json:"id"`
}
func (h *MultiUsersSvc) DelUser(c *gin.Context) {
userIDStr := c.Query(q.UserIDParam)
userID, err := strconv.ParseUint(userIDStr, 10, 64)
if err != nil {
c.JSON(q.ErrResp(c, 400, fmt.Errorf("invalid users ID %w", err)))
return
} else if userID == 0 {
c.JSON(q.ErrResp(c, 400, errors.New("It is not allowed to delete root")))
return
}
claims, err := h.getUserInfo(c)
if err != nil {
c.JSON(q.ErrResp(c, 401, err))
return
}
if claims[q.UserIDParam] == userIDStr {
c.JSON(q.ErrResp(c, 403, errors.New("can not delete self")))
return
}
// TODO: try to make following atomic
err = h.deps.Users().DelUser(userID)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
// TODO: move the folder to recycle bin when it failed to remove it
homePath := userIDStr
if err = h.deps.FS().Remove(homePath); err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
c.JSON(200, &DelUserResp{ID: userIDStr})
}
type ListUsersResp struct {
Users []*userstore.User `json:"users"`
}
func (h *MultiUsersSvc) ListUsers(c *gin.Context) {
// TODO: pagination is not enabled
// lastID := 0
// lastIDStr := c.Query(q.LastID)
// if lastIDStr != "" {
// lastID, err := strconv.Atoi(lastIDStr)
// if err != nil {
// c.JSON(q.ErrResp(c, 400, fmt.Errorf("invalid param %w", err)))
// return
// }
// }
users, err := h.deps.Users().ListUsers()
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
c.JSON(200, &ListUsersResp{Users: users})
}
type AddRoleReq struct { type AddRoleReq struct {
Role string `json:"role"` Role string `json:"role"`
} }
@ -405,3 +527,23 @@ func (h *MultiUsersSvc) isValidRole(role string) error {
} }
return h.isValidUserName(role) return h.isValidUserName(role)
} }
type SelfResp struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
}
func (h *MultiUsersSvc) Self(c *gin.Context) {
claims, err := h.getUserInfo(c)
if err != nil {
c.JSON(q.ErrResp(c, 401, err))
return
}
c.JSON(200, &SelfResp{
ID: claims[q.UserIDParam],
Name: claims[q.UserParam],
Role: claims[q.RoleParam],
})
}

View file

@ -13,6 +13,7 @@ var (
// dirs // dirs
UploadDir = "uploadings" UploadDir = "uploadings"
FsDir = "files" FsDir = "files"
FsRootDir = "files"
UserIDParam = "uid" UserIDParam = "uid"
UserParam = "user" UserParam = "user"
@ -21,6 +22,7 @@ var (
RoleParam = "role" RoleParam = "role"
ExpireParam = "expire" ExpireParam = "expire"
TokenCookie = "tk" TokenCookie = "tk"
LastID = "lid"
ErrAccessDenied = errors.New("access denied") ErrAccessDenied = errors.New("access denied")
ErrUnauthorized = errors.New("unauthorized") ErrUnauthorized = errors.New("unauthorized")
@ -131,6 +133,18 @@ func HomePath(userID, relFilePath string) string {
return filepath.Join(userID, relFilePath) return filepath.Join(userID, relFilePath)
} }
func FsRootPath(userID, relFilePath string) string {
return filepath.Join(userID, FsRootDir, relFilePath)
}
func GetTmpPath(userID, relFilePath string) string { func GetTmpPath(userID, relFilePath string) string {
return filepath.Join(UploadDir, userID, fmt.Sprintf("%x", sha1.Sum([]byte(relFilePath)))) return filepath.Join(UploadDir, userID, fmt.Sprintf("%x", sha1.Sum([]byte(relFilePath))))
} }
func UploadPath(userID, relFilePath string) string {
return filepath.Join(UploadFolder(userID), fmt.Sprintf("%x", sha1.Sum([]byte(relFilePath))))
}
func UploadFolder(userID string) string {
return filepath.Join(userID, UploadDir)
}

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 { func (bp *BoltPvd) Close() error {
return bp.db.Close() return bp.db.Close()
} }

View file

@ -8,6 +8,7 @@ var ErrNoLock = errors.New("no lock to unlock")
type IKVStore interface { type IKVStore interface {
AddNamespace(nsName string) error AddNamespace(nsName string) error
DelNamespace(nsName string) error DelNamespace(nsName string) error
HasNamespace(nsName string) bool
GetBool(key string) (bool, bool) GetBool(key string) (bool, bool)
GetBoolIn(ns, key string) (bool, bool) GetBoolIn(ns, key string) (bool, bool)
SetBool(key string, val bool) error SetBool(key string, val bool) error

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.POST("/logout", userHdrs.Logout)
usersAPI.GET("/isauthed", userHdrs.IsAuthed) usersAPI.GET("/isauthed", userHdrs.IsAuthed)
usersAPI.PATCH("/pwd", userHdrs.SetPwd) usersAPI.PATCH("/pwd", userHdrs.SetPwd)
usersAPI.PATCH("/pwd/force-set", userHdrs.ForceSetPwd)
usersAPI.POST("/", userHdrs.AddUser) usersAPI.POST("/", userHdrs.AddUser)
usersAPI.DELETE("/", userHdrs.DelUser)
usersAPI.GET("/list", userHdrs.ListUsers)
usersAPI.GET("/self", userHdrs.Self)
rolesAPI := v1.Group("/roles") rolesAPI := v1.Group("/roles")
rolesAPI.POST("/", userHdrs.AddRole) rolesAPI.POST("/", userHdrs.AddRole)
rolesAPI.DELETE("/", userHdrs.DelRole) rolesAPI.DELETE("/", userHdrs.DelRole)
rolesAPI.GET("/", userHdrs.ListRoles) rolesAPI.GET("/list", userHdrs.ListRoles)
filesAPI := v1.Group("/fs") filesAPI := v1.Group("/fs")
filesAPI.POST("/files", fileHdrs.Create) filesAPI.POST("/files", fileHdrs.Create)

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 package server
import ( import (
"crypto/sha1"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"math/rand" "math/rand"
"net/http"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"testing" "testing"
@ -65,120 +61,67 @@ func TestFileHandlers(t *testing.T) {
token := client.GetCookie(resp.Cookies(), q.TokenCookie) token := client.GetCookie(resp.Cookies(), q.TokenCookie)
cl := client.NewFilesClient(addr, token) cl := client.NewFilesClient(addr, token)
assertUploadOK := func(t *testing.T, filePath, content string) bool { // TODO: remove all files under home folder before testing
cl := client.NewFilesClient(addr, token) // or the count of files is incorrect
t.Run("ListHome", func(t *testing.T) {
files := map[string]string{
"0/files/home_file1": "12345678",
"0/files/home_file2": "12345678",
}
fileSize := int64(len([]byte(content))) for filePath, content := range files {
res, _, errs := cl.Create(filePath, fileSize) assertUploadOK(t, filePath, content, addr, token)
err = fs.Sync()
if err != nil {
t.Fatal(err)
}
}
resp, lhResp, errs := cl.ListHome()
if len(errs) > 0 { if len(errs) > 0 {
t.Error(errs) t.Fatal(errs)
return false } else if resp.StatusCode != 200 {
} else if res.StatusCode != 200 { t.Fatal(resp.StatusCode)
t.Error(res.StatusCode) } else if lhResp.Cwd != "0/files" {
return false t.Fatalf("incorrect ListHome cwd %s", lhResp.Cwd)
} else if len(lhResp.Metadatas) != len(files) {
for _, metadata := range lhResp.Metadatas {
fmt.Printf("%v\n", metadata)
}
t.Fatalf("incorrect ListHome content %d", len(lhResp.Metadatas))
} }
base64Content := base64.StdEncoding.EncodeToString([]byte(content)) infos := map[string]*fileshdr.MetadataResp{}
res, _, errs = cl.UploadChunk(filePath, base64Content, 0) for _, metadata := range lhResp.Metadatas {
if len(errs) > 0 { infos[metadata.Name] = metadata
t.Error(errs)
return false
} else if res.StatusCode != 200 {
t.Error(res.StatusCode)
return false
} }
return true if infos["home_file1"].Size != int64(len(files["0/files/home_file1"])) {
t.Fatalf("incorrect file size %d", infos["home_file1"].Size)
} else if infos["home_file1"].IsDir {
t.Fatal("incorrect item type")
}
if infos["home_file2"].Size != int64(len(files["0/files/home_file2"])) {
t.Fatalf("incorrect file size %d", infos["home_file2"].Size)
} else if infos["home_file2"].IsDir {
t.Fatal("incorrect item type")
} }
assetDownloadOK := func(t *testing.T, filePath, content string) bool {
var (
res *http.Response
body string
errs []error
fileSize = int64(len([]byte(content)))
)
// cl := client.NewFilesClient(addr)
rd := rand.Intn(3)
switch rd {
case 0:
res, body, errs = cl.Download(filePath, map[string]string{})
case 1:
res, body, errs = cl.Download(filePath, map[string]string{
"Range": fmt.Sprintf("bytes=0-%d", fileSize-1),
}) })
case 2:
res, body, errs = cl.Download(filePath, map[string]string{
"Range": fmt.Sprintf("bytes=0-%d, %d-%d", (fileSize-1)/2, (fileSize-1)/2+1, fileSize-1),
})
}
fileName := path.Base(filePath)
contentDispositionHeader := res.Header.Get("Content-Disposition")
if len(errs) > 0 {
t.Error(errs)
return false
}
if res.StatusCode != 200 && res.StatusCode != 206 {
t.Error(res.StatusCode)
return false
}
if contentDispositionHeader != fmt.Sprintf(`attachment; filename="%s"`, fileName) {
t.Errorf("incorrect Content-Disposition header: %s", contentDispositionHeader)
return false
}
switch rd {
case 0:
if body != content {
t.Errorf("body not equal got(%s) expect(%s)\n", body, content)
return false
}
case 1:
if body[2:] != content { // body returned by gorequest contains the first CRLF
t.Errorf("body not equal got(%s) expect(%s)\n", body[2:], content)
return false
}
default:
body = body[2:] // body returned by gorequest contains the first CRLF
realBody := ""
boundaryEnd := strings.Index(body, "\r\n")
boundary := body[0:boundaryEnd]
bodyParts := strings.Split(body, boundary)
for i, bodyPart := range bodyParts {
if i == 0 || i == len(bodyParts)-1 {
continue
}
start := strings.Index(bodyPart, "\r\n\r\n")
fmt.Printf("<%s>", bodyPart[start+4:len(bodyPart)-2]) // ignore the last CRLF
realBody += bodyPart[start+4 : len(bodyPart)-2]
}
if realBody != content {
t.Errorf("multi body not equal got(%s) expect(%s)\n", realBody, content)
return false
}
}
return true
}
t.Run("test uploading files with duplicated names", func(t *testing.T) { t.Run("test uploading files with duplicated names", func(t *testing.T) {
files := map[string]string{ files := map[string]string{
"0/dupdir/dup_file1": "12345678", "0/files/dupdir/dup_file1": "12345678",
"0/dupdir/dup_file2.ext": "12345678", "0/files/dupdir/dup_file2.ext": "12345678",
} }
renames := map[string]string{ renames := map[string]string{
"0/dupdir/dup_file1": "0/dupdir/dup_file1_1", "0/files/dupdir/dup_file1": "0/files/dupdir/dup_file1_1",
"0/dupdir/dup_file2.ext": "0/dupdir/dup_file2_1.ext", "0/files/dupdir/dup_file2.ext": "0/files/dupdir/dup_file2_1.ext",
} }
for filePath, content := range files { for filePath, content := range files {
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
assertUploadOK(t, filePath, content) assertUploadOK(t, filePath, content, addr, token)
err = fs.Sync() err = fs.Sync()
if err != nil { if err != nil {
@ -186,13 +129,13 @@ func TestFileHandlers(t *testing.T) {
} }
if i == 0 { if i == 0 {
assetDownloadOK(t, filePath, content) assertDownloadOK(t, filePath, content, addr, token)
} else if i == 1 { } else if i == 1 {
renamedFilePath, ok := renames[filePath] renamedFilePath, ok := renames[filePath]
if !ok { if !ok {
t.Fatal("new name not found") t.Fatal("new name not found")
} }
assetDownloadOK(t, renamedFilePath, content) assertDownloadOK(t, renamedFilePath, content, addr, token)
} }
} }
} }
@ -200,8 +143,8 @@ func TestFileHandlers(t *testing.T) {
t.Run("test files APIs: Create-UploadChunk-UploadStatus-Metadata-Delete", func(t *testing.T) { t.Run("test files APIs: Create-UploadChunk-UploadStatus-Metadata-Delete", func(t *testing.T) {
for filePath, content := range map[string]string{ for filePath, content := range map[string]string{
"0/path1/f1.md": "1111 1111 1111 1111", "0/files/path1/f1.md": "1111 1111 1111 1111",
"0/path1/path2/f2.md": "1010 1010 1111 0000 0010", "0/files/path1/path2/f2.md": "1010 1010 1111 0000 0010",
} { } {
fileSize := int64(len([]byte(content))) fileSize := int64(len([]byte(content)))
// create a file // create a file
@ -213,7 +156,7 @@ func TestFileHandlers(t *testing.T) {
} }
// check uploading file // check uploading file
uploadFilePath := path.Join(q.UploadDir, "0", fmt.Sprintf("%x", sha1.Sum([]byte(filePath)))) uploadFilePath := q.UploadPath("0", filePath)
info, err := fs.Stat(uploadFilePath) info, err := fs.Stat(uploadFilePath)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -290,11 +233,11 @@ func TestFileHandlers(t *testing.T) {
t.Run("test dirs APIs: Mkdir-Create-UploadChunk-List", func(t *testing.T) { t.Run("test dirs APIs: Mkdir-Create-UploadChunk-List", func(t *testing.T) {
for dirPath, files := range map[string]map[string]string{ for dirPath, files := range map[string]map[string]string{
"0/dir/path1": map[string]string{ "0/files/dir/path1": map[string]string{
"f1.md": "11111", "f1.md": "11111",
"f2.md": "22222222222", "f2.md": "22222222222",
}, },
"0/dir/path2/path2": map[string]string{ "0/files/dir/path2/path2": map[string]string{
"f3.md": "3333333", "f3.md": "3333333",
}, },
} { } {
@ -307,7 +250,7 @@ func TestFileHandlers(t *testing.T) {
for fileName, content := range files { for fileName, content := range files {
filePath := filepath.Join(dirPath, fileName) filePath := filepath.Join(dirPath, fileName)
assertUploadOK(t, filePath, content) assertUploadOK(t, filePath, content, addr, token)
} }
err = fs.Sync() err = fs.Sync()
@ -331,8 +274,8 @@ func TestFileHandlers(t *testing.T) {
}) })
t.Run("test operation APIs: Mkdir-Create-UploadChunk-Move-List", func(t *testing.T) { t.Run("test operation APIs: Mkdir-Create-UploadChunk-Move-List", func(t *testing.T) {
srcDir := "0/move/src" srcDir := "0/files/move/src"
dstDir := "0/move/dst" dstDir := "0/files/move/dst"
for _, dirPath := range []string{srcDir, dstDir} { for _, dirPath := range []string{srcDir, dstDir} {
res, _, errs := cl.Mkdir(dirPath) res, _, errs := cl.Mkdir(dirPath)
@ -352,7 +295,7 @@ func TestFileHandlers(t *testing.T) {
oldPath := filepath.Join(srcDir, fileName) oldPath := filepath.Join(srcDir, fileName)
newPath := filepath.Join(dstDir, fileName) newPath := filepath.Join(dstDir, fileName)
// fileSize := int64(len([]byte(content))) // fileSize := int64(len([]byte(content)))
assertUploadOK(t, oldPath, content) assertUploadOK(t, oldPath, content, addr, token)
res, _, errs := cl.Move(oldPath, newPath) res, _, errs := cl.Move(oldPath, newPath)
if len(errs) > 0 { if len(errs) > 0 {
@ -383,17 +326,17 @@ func TestFileHandlers(t *testing.T) {
t.Run("test download APIs: Download(normal, ranges)", func(t *testing.T) { t.Run("test download APIs: Download(normal, ranges)", func(t *testing.T) {
for filePath, content := range map[string]string{ for filePath, content := range map[string]string{
"0/download/path1/f1": "123456", "0/files/download/path1/f1": "123456",
"0/download/path1/path2": "12345678", "0/files/download/path1/path2": "12345678",
} { } {
assertUploadOK(t, filePath, content) assertUploadOK(t, filePath, content, addr, token)
err = fs.Sync() err = fs.Sync()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
assetDownloadOK(t, filePath, content) assertDownloadOK(t, filePath, content, addr, token)
} }
}) })
@ -407,7 +350,7 @@ func TestFileHandlers(t *testing.T) {
startClient := func(files []*mockFile) { startClient := func(files []*mockFile) {
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
for _, file := range files { for _, file := range files {
if !assertUploadOK(t, fmt.Sprintf("%s_%d", file.FilePath, i), file.Content) { if !assertUploadOK(t, fmt.Sprintf("%s_%d", file.FilePath, i), file.Content, addr, token) {
break break
} }
@ -416,7 +359,7 @@ func TestFileHandlers(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if !assetDownloadOK(t, fmt.Sprintf("%s_%d", file.FilePath, i), file.Content) { if !assertDownloadOK(t, fmt.Sprintf("%s_%d", file.FilePath, i), file.Content, addr, token) {
break break
} }
} }
@ -446,10 +389,18 @@ func TestFileHandlers(t *testing.T) {
wg.Wait() wg.Wait()
}) })
t.Run("test uploading APIs: Create, ListUploadings, DelUploading", func(t *testing.T) { t.Run("test uploading APIs: ListUploadings, Create, ListUploadings, DelUploading", func(t *testing.T) {
// it should return no error even no file is uploaded
res, lResp, errs := cl.ListUploadings()
if len(errs) > 0 {
t.Fatal(errs)
} else if res.StatusCode != 200 {
t.Fatal(res.StatusCode)
}
files := map[string]string{ files := map[string]string{
"0/uploadings/path1/f1": "123456", "0/files/uploadings/path1/f1": "123456",
"0/uploadings/path1/path2": "12345678", "0/files/uploadings/path1/path2": "12345678",
} }
for filePath, content := range files { for filePath, content := range files {
@ -462,7 +413,7 @@ func TestFileHandlers(t *testing.T) {
} }
} }
res, lResp, errs := cl.ListUploadings() res, lResp, errs = cl.ListUploadings()
if len(errs) > 0 { if len(errs) > 0 {
t.Fatal(errs) t.Fatal(errs)
} else if res.StatusCode != 200 { } else if res.StatusCode != 200 {
@ -507,7 +458,7 @@ func TestFileHandlers(t *testing.T) {
// cl := client.NewFilesClient(addr) // cl := client.NewFilesClient(addr)
files := map[string]string{ files := map[string]string{
"0/uploadings/path1/f1": "12345678", "0/files/uploadings/path1/f1": "12345678",
} }
for filePath, content := range files { for filePath, content := range files {
@ -554,7 +505,7 @@ func TestFileHandlers(t *testing.T) {
t.Fatal("incorrect uploaded size", mRes) t.Fatal("incorrect uploaded size", mRes)
} }
assetDownloadOK(t, filePath, content) assertDownloadOK(t, filePath, content, addr, token)
} }
}) })
@ -562,9 +513,9 @@ func TestFileHandlers(t *testing.T) {
// cl := client.NewFilesClient(addr) // cl := client.NewFilesClient(addr)
files := map[string]string{ files := map[string]string{
"0/uploadings/random/path1/f1": "12345678", "0/files/uploadings/random/path1/f1": "12345678",
"0/uploadings/random/path1/f2": "87654321", "0/files/uploadings/random/path1/f2": "87654321",
"0/uploadings/random/path1/f3": "17654321", "0/files/uploadings/random/path1/f3": "17654321",
} }
for filePath, content := range files { for filePath, content := range files {
@ -622,7 +573,7 @@ func TestFileHandlers(t *testing.T) {
t.Fatalf("file content not equal: %s", filePath) t.Fatalf("file content not equal: %s", filePath)
} }
assetDownloadOK(t, filePath, content) assertDownloadOK(t, filePath, content, addr, token)
} }
}) })

View file

@ -3,14 +3,16 @@ package server
import ( import (
"fmt" "fmt"
"os" "os"
"strconv"
"testing" "testing"
"github.com/ihexxa/quickshare/src/client" "github.com/ihexxa/quickshare/src/client"
q "github.com/ihexxa/quickshare/src/handlers"
su "github.com/ihexxa/quickshare/src/handlers/singleuserhdr" su "github.com/ihexxa/quickshare/src/handlers/singleuserhdr"
"github.com/ihexxa/quickshare/src/userstore" "github.com/ihexxa/quickshare/src/userstore"
) )
func TestSingleUserHandlers(t *testing.T) { func TestUsersHandlers(t *testing.T) {
addr := "http://127.0.0.1:8686" addr := "http://127.0.0.1:8686"
root := "testData" root := "testData"
config := `{ config := `{
@ -41,6 +43,7 @@ func TestSingleUserHandlers(t *testing.T) {
srv := startTestServer(config) srv := startTestServer(config)
defer srv.Shutdown() defer srv.Shutdown()
fs := srv.depsFS()
usersCl := client.NewSingleUserClient(addr) usersCl := client.NewSingleUserClient(addr)
@ -48,7 +51,7 @@ func TestSingleUserHandlers(t *testing.T) {
t.Fatal("fail to start server") t.Fatal("fail to start server")
} }
t.Run("test users APIs: Login-SetPwd-Logout-Login", func(t *testing.T) { t.Run("test users APIs: Login-Self-SetPwd-Logout-Login", func(t *testing.T) {
resp, _, errs := usersCl.Login(adminName, adminPwd) resp, _, errs := usersCl.Login(adminName, adminPwd)
if len(errs) > 0 { if len(errs) > 0 {
t.Fatal(errs) t.Fatal(errs)
@ -58,6 +61,17 @@ func TestSingleUserHandlers(t *testing.T) {
token := client.GetCookie(resp.Cookies(), su.TokenCookie) token := client.GetCookie(resp.Cookies(), su.TokenCookie)
resp, selfResp, errs := usersCl.Self(token)
if len(errs) > 0 {
t.Fatal(errs)
} else if resp.StatusCode != 200 {
t.Fatal(resp.StatusCode)
} else if selfResp.ID != "0" ||
selfResp.Name != adminName ||
selfResp.Role != userstore.AdminRole {
t.Fatalf("user infos don't match %v", selfResp)
}
resp, _, errs = usersCl.SetPwd(adminPwd, adminNewPwd, token) resp, _, errs = usersCl.SetPwd(adminPwd, adminNewPwd, token)
if len(errs) > 0 { if len(errs) > 0 {
t.Fatal(errs) t.Fatal(errs)
@ -90,7 +104,7 @@ func TestSingleUserHandlers(t *testing.T) {
token := client.GetCookie(resp.Cookies(), su.TokenCookie) token := client.GetCookie(resp.Cookies(), su.TokenCookie)
userName, userPwd := "user", "1234" userName, userPwd := "user_login", "1234"
resp, auResp, errs := usersCl.AddUser(userName, userPwd, userstore.UserRole, token) resp, auResp, errs := usersCl.AddUser(userName, userPwd, userstore.UserRole, token)
if len(errs) > 0 { if len(errs) > 0 {
t.Fatal(errs) t.Fatal(errs)
@ -100,6 +114,18 @@ func TestSingleUserHandlers(t *testing.T) {
// TODO: check id // TODO: check id
fmt.Printf("new user id: %v\n", auResp) fmt.Printf("new user id: %v\n", auResp)
// check uploading file
userFsRootFolder := q.FsRootPath(auResp.ID, "/")
_, err = fs.Stat(userFsRootFolder)
if err != nil {
t.Fatal(err)
}
userUploadFolder := q.UploadFolder(auResp.ID)
_, err = fs.Stat(userUploadFolder)
if err != nil {
t.Fatal(err)
}
resp, _, errs = usersCl.Logout(token) resp, _, errs = usersCl.Logout(token)
if len(errs) > 0 { if len(errs) > 0 {
t.Fatal(errs) t.Fatal(errs)
@ -114,6 +140,85 @@ func TestSingleUserHandlers(t *testing.T) {
t.Fatal(resp.StatusCode) t.Fatal(resp.StatusCode)
} }
resp, _, errs = usersCl.DelUser(auResp.ID, token)
if len(errs) > 0 {
t.Fatal(errs)
} else if resp.StatusCode != 200 {
t.Fatal(resp.StatusCode)
}
resp, _, errs = usersCl.Logout(token)
if len(errs) > 0 {
t.Fatal(errs)
} else if resp.StatusCode != 200 {
t.Fatal(resp.StatusCode)
}
})
t.Run("test users APIs: Login-AddUser-ListUsers-DelUser-ListUsers", func(t *testing.T) {
resp, _, errs := usersCl.Login(adminName, adminNewPwd)
if len(errs) > 0 {
t.Fatal(errs)
} else if resp.StatusCode != 200 {
t.Fatal(resp.StatusCode)
}
token := client.GetCookie(resp.Cookies(), su.TokenCookie)
userName, userPwd, userRole := "user_admin", "1234", userstore.UserRole
resp, auResp, errs := usersCl.AddUser(userName, userPwd, userRole, token)
if len(errs) > 0 {
t.Fatal(errs)
} else if resp.StatusCode != 200 {
t.Fatal(resp.StatusCode)
}
// TODO: check id
fmt.Printf("new user id: %v\n", auResp)
newUserID, err := strconv.ParseUint(auResp.ID, 10, 64)
if err != nil {
t.Fatal(err)
}
resp, lsResp, errs := usersCl.ListUsers(token)
if len(errs) > 0 {
t.Fatal(errs)
} else if resp.StatusCode != 200 {
t.Fatal(resp.StatusCode)
}
if len(lsResp.Users) != 2 {
t.Fatal(fmt.Errorf("incorrect users size (%d)", len(lsResp.Users)))
} else if lsResp.Users[0].ID != 0 ||
lsResp.Users[0].Name != adminName ||
lsResp.Users[0].Role != userstore.AdminRole {
t.Fatal(fmt.Errorf("incorrect root info (%v)", lsResp.Users[0]))
} else if lsResp.Users[1].ID != newUserID ||
lsResp.Users[1].Name != userName ||
lsResp.Users[1].Role != userRole {
t.Fatal(fmt.Errorf("incorrect user info (%v)", lsResp.Users[1]))
}
resp, _, errs = usersCl.DelUser(auResp.ID, token)
if len(errs) > 0 {
t.Fatal(errs)
} else if resp.StatusCode != 200 {
t.Fatal(resp.StatusCode)
}
resp, lsResp, errs = usersCl.ListUsers(token)
if len(errs) > 0 {
t.Fatal(errs)
} else if resp.StatusCode != 200 {
t.Fatal(resp.StatusCode)
}
if len(lsResp.Users) != 1 {
t.Fatal(fmt.Errorf("incorrect users size (%d)", len(lsResp.Users)))
} else if lsResp.Users[0].ID != 0 ||
lsResp.Users[0].Name != adminName ||
lsResp.Users[0].Role != userstore.AdminRole {
t.Fatal(fmt.Errorf("incorrect root info (%v)", lsResp.Users[0]))
}
resp, _, errs = usersCl.Logout(token) resp, _, errs = usersCl.Logout(token)
if len(errs) > 0 { if len(errs) > 0 {
t.Fatal(errs) t.Fatal(errs)

View file

@ -1,8 +1,14 @@
package server package server
import ( import (
"encoding/base64"
"fmt"
"io/ioutil" "io/ioutil"
// "path" "math/rand"
"net/http"
"path"
"strings"
"testing"
"time" "time"
"github.com/ihexxa/gocfg" "github.com/ihexxa/gocfg"
@ -64,3 +70,104 @@ func compareFileContent(fs fspkg.ISimpleFS, uid, filePath string, expectedConten
return string(gotContent) == expectedContent, nil return string(gotContent) == expectedContent, nil
} }
func assertUploadOK(t *testing.T, filePath, content, addr string, token *http.Cookie) bool {
cl := client.NewFilesClient(addr, token)
fileSize := int64(len([]byte(content)))
res, _, errs := cl.Create(filePath, fileSize)
if len(errs) > 0 {
t.Error(errs)
return false
} else if res.StatusCode != 200 {
t.Error(res.StatusCode)
return false
}
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
res, _, errs = cl.UploadChunk(filePath, base64Content, 0)
if len(errs) > 0 {
t.Error(errs)
return false
} else if res.StatusCode != 200 {
t.Error(res.StatusCode)
return false
}
return true
}
func assertDownloadOK(t *testing.T, filePath, content, addr string, token *http.Cookie) bool {
var (
res *http.Response
body string
errs []error
fileSize = int64(len([]byte(content)))
)
cl := client.NewFilesClient(addr, token)
rd := rand.Intn(3)
switch rd {
case 0:
res, body, errs = cl.Download(filePath, map[string]string{})
case 1:
res, body, errs = cl.Download(filePath, map[string]string{
"Range": fmt.Sprintf("bytes=0-%d", fileSize-1),
})
case 2:
res, body, errs = cl.Download(filePath, map[string]string{
"Range": fmt.Sprintf("bytes=0-%d, %d-%d", (fileSize-1)/2, (fileSize-1)/2+1, fileSize-1),
})
}
fileName := path.Base(filePath)
contentDispositionHeader := res.Header.Get("Content-Disposition")
if len(errs) > 0 {
t.Error(errs)
return false
}
if res.StatusCode != 200 && res.StatusCode != 206 {
t.Error(res.StatusCode)
return false
}
if contentDispositionHeader != fmt.Sprintf(`attachment; filename="%s"`, fileName) {
t.Errorf("incorrect Content-Disposition header: %s", contentDispositionHeader)
return false
}
switch rd {
case 0:
if body != content {
t.Errorf("body not equal got(%s) expect(%s)\n", body, content)
return false
}
case 1:
if body[2:] != content { // body returned by gorequest contains the first CRLF
t.Errorf("body not equal got(%s) expect(%s)\n", body[2:], content)
return false
}
default:
body = body[2:] // body returned by gorequest contains the first CRLF
realBody := ""
boundaryEnd := strings.Index(body, "\r\n")
boundary := body[0:boundaryEnd]
bodyParts := strings.Split(body, boundary)
for i, bodyPart := range bodyParts {
if i == 0 || i == len(bodyParts)-1 {
continue
}
start := strings.Index(bodyPart, "\r\n\r\n")
fmt.Printf("<%s>", bodyPart[start+4:len(bodyPart)-2]) // ignore the last CRLF
realBody += bodyPart[start+4 : len(bodyPart)-2]
}
if realBody != content {
t.Errorf("multi body not equal got(%s) expect(%s)\n", realBody, content)
return false
}
}
return true
}

View file

@ -26,20 +26,22 @@ const (
) )
type User struct { type User struct {
ID uint64 ID uint64 `json:"id,string"`
Name string Name string `json:"name"`
Pwd string Pwd string `json:"pwd"`
Role string Role string `json:"role"`
} }
type IUserStore interface { type IUserStore interface {
Init(rootName, rootPwd string) error Init(rootName, rootPwd string) error
IsInited() bool IsInited() bool
AddUser(user *User) error AddUser(user *User) error
DelUser(id uint64) error
GetUser(id uint64) (*User, error) GetUser(id uint64) (*User, error)
GetUserByName(name string) (*User, error) GetUserByName(name string) (*User, error)
SetName(id uint64, name string) error SetName(id uint64, name string) error
SetPwd(id uint64, pwd string) error SetPwd(id uint64, pwd string) error
ListUsers() ([]*User, error)
SetRole(id uint64, role string) error SetRole(id uint64, role string) error
AddRole(role string) error AddRole(role string) error
DelRole(role string) error DelRole(role string) error
@ -135,6 +137,27 @@ func (us *KVUserStore) AddUser(user *User) error {
return us.store.SetStringIn(RolesNs, userID, user.Role) return us.store.SetStringIn(RolesNs, userID, user.Role)
} }
func (us *KVUserStore) DelUser(id uint64) error {
us.mtx.Lock()
defer us.mtx.Unlock()
userID := fmt.Sprint(id)
name, ok := us.store.GetStringIn(NamesNs, userID)
if !ok {
return fmt.Errorf("userID (%s) exists", userID)
}
// TODO: add complement operations if part of the actions fails
err1 := us.store.DelStringIn(NamesNs, userID)
err2 := us.store.DelStringIn(IDsNs, name)
err3 := us.store.DelStringIn(PwdsNs, userID)
err4 := us.store.DelStringIn(RolesNs, userID)
if err1 != nil || err2 != nil || err3 != nil || err4 != nil {
return fmt.Errorf("get name(%s) id(%s) pwd(%s) role(%s)", err1, err2, err3, err4)
}
return nil
}
func (us *KVUserStore) GetUser(id uint64) (*User, error) { func (us *KVUserStore) GetUser(id uint64) (*User, error) {
us.mtx.RLock() us.mtx.RLock()
defer us.mtx.RUnlock() defer us.mtx.RUnlock()
@ -258,6 +281,37 @@ func (us *KVUserStore) SetRole(id uint64, role string) error {
return us.store.SetStringIn(RolesNs, userID, role) return us.store.SetStringIn(RolesNs, userID, role)
} }
func (us *KVUserStore) ListUsers() ([]*User, error) {
us.mtx.RLock()
defer us.mtx.RUnlock()
idToName, err := us.store.ListStringsIn(NamesNs)
if err != nil {
return nil, err
}
roles, err := us.store.ListStringsIn(RolesNs)
if err != nil {
return nil, err
}
users := []*User{}
for id, name := range idToName {
intID, err := strconv.ParseUint(id, 10, 64)
if err != nil {
return nil, err
}
users = append(users, &User{
ID: intID,
Name: name,
Role: roles[id],
})
}
return users, nil
}
func (us *KVUserStore) AddRole(role string) error { func (us *KVUserStore) AddRole(role string) error {
us.mtx.Lock() us.mtx.Lock()
defer us.mtx.Unlock() defer us.mtx.Unlock()

View file

@ -51,6 +51,20 @@ func TestUserStores(t *testing.T) {
t.Fatalf("roles not matched %s %s", role1, user.Role) t.Fatalf("roles not matched %s %s", role1, user.Role)
} }
users, err := store.ListUsers()
if err != nil {
t.Fatal(err)
}
if len(users) != 2 {
t.Fatalf("users size should be 2 (%d)", len(users))
}
if users[0].ID != 0 || users[0].Name != rootName || users[0].Role != AdminRole {
t.Fatalf("incorrect root info %v", users[0])
}
if users[1].ID != 1 || users[1].Name != name1 || users[1].Role != role1 {
t.Fatalf("incorrect user info %v", users[1])
}
err = store.SetName(id, name2) err = store.SetName(id, name2)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -91,6 +105,22 @@ func TestUserStores(t *testing.T) {
if user.Role != role2 { if user.Role != role2 {
t.Fatalf("roles not matched %s %s", role2, user.Role) t.Fatalf("roles not matched %s %s", role2, user.Role)
} }
err = store.DelUser(id)
if err != nil {
t.Fatal(err)
}
users, err = store.ListUsers()
if err != nil {
t.Fatal(err)
}
if len(users) != 1 {
t.Fatalf("users size should be 2 (%d)", len(users))
}
if users[0].ID != 0 || users[0].Name != rootName || users[0].Role != AdminRole {
t.Fatalf("incorrect root info %v", users[0])
}
} }
testRoleMethods := func(t *testing.T, store IUserStore) { testRoleMethods := func(t *testing.T, store IUserStore) {