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