diff --git a/public/static/css/style.css b/public/static/css/style.css index 0d2d583..302f115 100644 --- a/public/static/css/style.css +++ b/public/static/css/style.css @@ -69,21 +69,26 @@ right: 0; top: 0; bottom: 0; - background-color: rgba(0, 0, 0, 0.75); + background-color: rgba(0, 0, 0, 0.5); z-index: 100; overflow: scroll; } -#panes .container { +#panes .root-container { max-width: 80rem; width: 96%; - background-color: white; z-index: 101; text-align: left; margin: 3rem auto 8rem auto; border-radius: 0.6rem; } +#panes .container { + background-color: white; + margin: 3rem auto 1rem auto; + border-radius: 0.6rem; +} + #panes .return-btn { position: fixed; max-width: 960px; @@ -133,12 +138,17 @@ border-top: solid 1px transparent; } -#item-list .dot { +.container .dot { overflow: hidden; margin-left: 1rem; margin-right: 1rem; } +#panes .dot { + overflow: hidden; + margin-left: 0; +} + #item-list .vbar { overflow: hidden; margin: 1.5rem 1rem; @@ -164,6 +174,14 @@ display: block; } +.item-name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + overflow-wrap: break-word; + display: block; +} + #item-list .item-op { line-height: 4rem; } diff --git a/src/client/files.go b/src/client/files.go index e6282a6..e80d3d2 100644 --- a/src/client/files.go +++ b/src/client/files.go @@ -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). diff --git a/src/client/singleuser.go b/src/client/users.go similarity index 67% rename from src/client/singleuser.go rename to src/client/users.go index 2f12c52..3b12e5f 100644 --- a/src/client/singleuser.go +++ b/src/client/users.go @@ -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 +} diff --git a/src/client/web/src/client/index.ts b/src/client/web/src/client/index.ts index c80214a..52f3c34 100644 --- a/src/client/web/src/client/index.ts +++ b/src/client/web/src/client/index.ts @@ -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; +} + +export interface ListRolesResp { + roles: Array; } export interface MetadataResp { @@ -42,7 +51,15 @@ export interface IUsersClient { login: (user: string, pwd: string) => Promise; logout: () => Promise; isAuthed: () => Promise; + self: () => Promise; setPwd: (oldPwd: string, newPwd: string) => Promise; + forceSetPwd: (userID: string, newPwd: string) => Promise; + addUser: (name: string, pwd: string, role: string) => Promise; + delUser: (userID: string) => Promise; + listUsers: () => Promise; + addRole: (role: string) => Promise; + delRole: (role: string) => Promise; + listRoles: () => Promise; } export interface IFilesClient { diff --git a/src/client/web/src/client/users.ts b/src/client/web/src/client/users.ts index 2def297..c7037e9 100644 --- a/src/client/web/src/client/users.ts +++ b/src/client/web/src/client/users.ts @@ -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 => { 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 => { return this.do({ method: "patch", @@ -43,8 +41,19 @@ export class UsersClient extends BaseClient { }); }; + forceSetPwd = (userID: string, newPwd: string): Promise => { + 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 => { + addUser = (name: string, pwd: string, role: string): Promise => { return this.do({ method: "post", url: `${this.url}/v1/users/`, @@ -55,4 +64,54 @@ export class UsersClient extends BaseClient { }, }); }; + + delUser = (userID: string): Promise => { + return this.do({ + method: "delete", + url: `${this.url}/v1/users/`, + params: { + [userIDParam]: userID, + }, + }); + }; + + listUsers = (): Promise => { + return this.do({ + method: "get", + url: `${this.url}/v1/users/list`, + params: {}, + }); + }; + + addRole = (role: string): Promise => { + return this.do({ + method: "post", + url: `${this.url}/v1/roles/`, + data: { role }, + }); + }; + + delRole = (role: string): Promise => { + return this.do({ + method: "delete", + url: `${this.url}/v1/roles/`, + data: { role }, + }); + }; + + listRoles = (): Promise => { + return this.do({ + method: "get", + url: `${this.url}/v1/roles/list`, + params: {}, + }); + }; + + self = (): Promise => { + return this.do({ + method: "get", + url: `${this.url}/v1/users/self`, + params: {}, + }); + }; } diff --git a/src/client/web/src/client/users_mock.ts b/src/client/web/src/client/users_mock.ts index 4d45e72..a1b62a8 100644 --- a/src/client/web/src/client/users_mock.ts +++ b/src/client/web/src/client/users_mock.ts @@ -7,7 +7,14 @@ export class MockUsersClient { private logoutMockResp: Promise; private isAuthedMockResp: Promise; private setPwdMockResp: Promise; + private forceSetPwdMockResp: Promise; private addUserMockResp: Promise; + private delUserMockResp: Promise; + private listUsersMockResp: Promise; + private addRoleMockResp: Promise; + private delRoleMockResp: Promise; + private listRolesMockResp: Promise; + private selfMockResp: Promise; constructor(url: string) { this.url = url; @@ -25,9 +32,30 @@ export class MockUsersClient { setPwdMock = (resp: Promise) => { this.setPwdMockResp = resp; } + forceSetPwdMock = (resp: Promise) => { + this.forceSetPwdMockResp = resp; + } addUserMock = (resp: Promise) => { this.addUserMockResp = resp; } + delUserMock = (resp: Promise) => { + this.delUserMockResp = resp; + } + listUsersMock = (resp: Promise) => { + this.listUsersMockResp = resp; + } + addRoleMock = (resp: Promise) => { + this.addRoleMockResp = resp; + } + delRoleMock = (resp: Promise) => { + this.delRoleMockResp = resp; + } + listRolesMock = (resp: Promise) => { + this.listRolesMockResp = resp; + } + slefMock = (resp: Promise) => { + this.selfMockResp = resp; + } login = (user: string, pwd: string): Promise => { return this.loginMockResp; @@ -44,9 +72,36 @@ export class MockUsersClient { setPwd = (oldPwd: string, newPwd: string): Promise => { return this.setPwdMockResp; } + + forceSetPwd = (userID: string, newPwd: string): Promise => { + return this.forceSetPwdMockResp; + } addUser = (name: string, pwd: string, role: string): Promise => { return this.addUserMockResp; } + delUser = (userID: string): Promise => { + return this.delUserMockResp; + } + + listUsers = (): Promise => { + return this.listUsersMockResp; + } + + addRole = (role: string): Promise => { + return this.addRoleMockResp; + } + + delRole = (role: string): Promise => { + return this.delRoleMockResp; + } + + listRoles = (): Promise => { + return this.listRolesMockResp; + } + + self = (): Promise => { + return this.selfMockResp; + } } diff --git a/src/client/web/src/components/browser.tsx b/src/client/web/src/components/browser.tsx index 8319e19..05aeb06 100644 --- a/src/client/web/src/components/browser.tsx +++ b/src/client/web/src/components/browser.tsx @@ -259,52 +259,6 @@ export class Browser extends React.Component { const sizeCellClass = this.props.isVertical ? `hidden margin-s` : ``; const modTimeCellClass = this.props.isVertical ? `hidden margin-s` : ``; - const layoutChildren = [ - , - , - - - - , - - - - , - ]; - - // const ops = ( - // - // ); - const ops = (
diff --git a/src/client/web/src/components/browser.updater.ts b/src/client/web/src/components/browser.updater.ts index ba3df12..7014fb3 100644 --- a/src/client/web/src/components/browser.updater.ts +++ b/src/client/web/src/components/browser.updater.ts @@ -124,17 +124,6 @@ export class Updater { : this.props.items; }; - goHome = async (): Promise => { - const listResp = await this.filesClient.listHome(); - - // how to get current dir? to dirPath? - // this.props.dirPath = dirParts; - this.props.items = - listResp.status === 200 - ? List(listResp.data.metadatas) - : this.props.items; - }; - moveHere = async ( srcDir: string, dstDir: string, diff --git a/src/client/web/src/components/core_state.ts b/src/client/web/src/components/core_state.ts index 7abc5f1..6917107 100644 --- a/src/client/web/src/components/core_state.ts +++ b/src/client/web/src/components/core_state.ts @@ -60,16 +60,17 @@ export function initState(): ICoreState { uploadFiles: List([]), }, panes: { + userRole: "", displaying: "", - paneNames: Set(["settings", "login"]), + paneNames: Set(["settings", "login", "admin"]), login: { authed: false, }, + admin: { + users: Map(), + roles: Set(), + }, }, - admin: { - users: Map(), - roles: Set() - } }, }; } @@ -92,16 +93,17 @@ export function mockState(): ICoreState { uploadFiles: List([]), }, panes: { + userRole: "", displaying: "", - paneNames: Set(["settings", "login"]), + paneNames: Set(["settings", "login", "admin"]), login: { authed: false, }, + admin: { + users: Map(), + roles: Set(), + }, }, - admin: { - users: Map(), - roles: Set() - } }, }; } diff --git a/src/client/web/src/components/pane_admin.tsx b/src/client/web/src/components/pane_admin.tsx index 116c98b..0d29ada 100644 --- a/src/client/web/src/components/pane_admin.tsx +++ b/src/client/web/src/components/pane_admin.tsx @@ -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; @@ -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 => { - // const resp = await Updater.client.add - // } - - // static login = async (user: string, pwd: string): Promise => { - // const resp = await Updater.client.login(user, pwd); - // Updater.setAuthed(resp.status === 200); - // return resp.status === 200; - // }; - - // static logout = async (): Promise => { - // const resp = await Updater.client.logout(); - // Updater.setAuthed(false); - // return resp.status === 200; - // }; - - // static isAuthed = async (): Promise => { - // const resp = await Updater.client.isAuthed(); - // return resp.status === 200; - // }; - - // static initIsAuthed = async (): Promise => { - // 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; + 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 { -// 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) => { + this.setState({ newPwd1: ev.target.value }); + }; + changePwd2 = (ev: React.ChangeEvent) => { + this.setState({ newPwd2: ev.target.value }); + }; + changeRole = (ev: React.ChangeEvent) => { + this.setState({ role: ev.target.value }); + }; -// changeUser = (ev: React.ChangeEvent) => { -// 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) => { -// 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 ( + + +
+ +
+
Name: {this.props.name}
+
+ ID: {this.props.id} / Role: {this.props.role} +
+
+
+
+
+ +
-// logout = () => { -// Updater.logout().then((ok: boolean) => { -// if (ok) { -// this.update(Updater.setAuthPane); -// } else { -// alert("Failed to logout."); -// } -// }); -// }; + {/* no API yet */} + {/*
+ + +
*/} +
+
-// render() { -// const elements: Array = [ -// , -// , -// , -// ]; +
+ + + +
-// return ( -// -//
-// {/*
Login
*/} -// -//
+
+ ); + } +} -// -// -// -//
-// ); -// } -// } +export interface State { + newUserName: string; + newUserPwd1: string; + newUserPwd2: string; + newUserRole: string; + newRole: string; +} +export class AdminPane extends React.Component { + constructor(p: Props) { + super(p); + this.state = { + newUserName: "", + newUserPwd1: "", + newUserPwd2: "", + newUserRole: "", + newRole: "", + }; + } + + onChangeUserName = (ev: React.ChangeEvent) => { + this.setState({ newUserName: ev.target.value }); + }; + onChangeUserPwd1 = (ev: React.ChangeEvent) => { + this.setState({ newUserPwd1: ev.target.value }); + }; + onChangeUserPwd2 = (ev: React.ChangeEvent) => { + this.setState({ newUserPwd2: ev.target.value }); + }; + onChangeUserRole = (ev: React.ChangeEvent) => { + this.setState({ newUserRole: ev.target.value }); + }; + onChangeRole = (ev: React.ChangeEvent) => { + 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 ( +
+ +
+ ); + }); + + const roleList = this.props.roles.valueSeq().map((role: string) => { + return ( +
+
+ + {role} +
+
+ +
+
+ ); + }); + + return ( +
+
+
+ {/* */} +
+ + + + +
+
+ +
+ {/*
*/} +
+
+ +
+
+
+ + + Users + + +
+ {userList} +
+
+ +
+
+
+ + + +
+
+ +
+
+
+ +
+
+
+ + + Roles + + +
+ {roleList} +
+
+
+ ); + } +} diff --git a/src/client/web/src/components/pane_login.tsx b/src/client/web/src/components/pane_login.tsx index 3b60679..08ab757 100644 --- a/src/client/web/src/components/pane_login.tsx +++ b/src/client/web/src/components/pane_login.tsx @@ -159,11 +159,13 @@ export class AuthPane extends React.Component { return (
- {/*
Login
*/} - +
+ {/*
Login
*/} + +
diff --git a/src/client/web/src/components/pane_settings.tsx b/src/client/web/src/components/pane_settings.tsx index e0d5284..4938e71 100644 --- a/src/client/web/src/components/pane_settings.tsx +++ b/src/client/web/src/components/pane_settings.tsx @@ -120,58 +120,63 @@ export class PaneSettings extends React.Component { ]; return ( -
-
-
-
-
Update Password
+
+
+
+
+
+
Update Password
+
+
+ +
-
- + +
+ +
+
+ +
+
+
- -
-
- - -
-
- -
- -
-
-
-
Logout
-
-
- +
+
+
Logout
+
+
+ +
diff --git a/src/client/web/src/components/panes.tsx b/src/client/web/src/components/panes.tsx index 3f9d449..b18e667 100644 --- a/src/client/web/src/components/panes.tsx +++ b/src/client/web/src/components/panes.tsx @@ -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; 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 => { + 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 => { + 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 => { + const resp = await Updater.client.delUser(userID); + return resp.status === 200; + }; + + static setRole = async (userID: string, role: string): Promise => { + const resp = await Updater.client.delUser(userID); + return resp.status === 200; + }; + + static forceSetPwd = async ( + userID: string, + pwd: string + ): Promise => { + const resp = await Updater.client.forceSetPwd(userID, pwd); + return resp.status === 200; + }; + + static listUsers = async (): Promise => { + const resp = await Updater.client.listUsers(); + if (resp.status !== 200) { + return false; + } + + const lsRes = resp.data as ListUsersResp; + let users = Map({}); + lsRes.users.forEach((user: User) => { + users = users.set(user.name, user); + }); + Updater.props.admin.users = users; + + return true; + }; + + static addRole = async (role: string): Promise => { + const resp = await Updater.client.addRole(role); + // TODO: should return uid instead + return resp.status === 200; + }; + + static delRole = async (role: string): Promise => { + const resp = await Updater.client.delRole(role); + return resp.status === 200; + }; + + static listRoles = async (): Promise => { + const resp = await Updater.client.listRoles(); + if (resp.status !== 200) { + return false; + } + + const lsRes = resp.data as ListRolesResp; + let roles = Set(); + 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 { constructor(p: Props) { super(p); Updater.init(p); + Updater.setClient(new UsersClient("")); } closePane = () => { @@ -63,7 +151,7 @@ export class Panes extends React.Component { displaying = "login"; } - const panesMap: Map = Map({ + let panesMap: Map = Map({ settings: ( ), @@ -72,6 +160,17 @@ export class Panes extends React.Component { ), }); + if (this.props.userRole === "admin") { + panesMap = panesMap.set( + "admin", + + ); + } + 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 { const btnClass = displaying === "login" ? "hidden" : ""; return (
-
-
-

{displaying}

-
- +
+
+
+

{displaying}

+
+ +
-
{panes} - -
+ {/*
*/} + {/*
*/}
); } diff --git a/src/client/web/src/components/root_frame.tsx b/src/client/web/src/components/root_frame.tsx index 28b620c..7415d1b 100644 --- a/src/client/web/src/components/root_frame.tsx +++ b/src/client/web/src/components/root_frame.tsx @@ -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 { this.props.update(PanesUpdater.updateState); }; + showAdmin = () => { + PanesUpdater.displayPane("admin"); + this.props.update(PanesUpdater.updateState); + }; + render() { const update = this.props.update; return (
@@ -68,6 +74,12 @@ export class RootFrame extends React.Component { > Settings +
diff --git a/src/client/web/src/components/state_mgr.tsx b/src/client/web/src/components/state_mgr.tsx index 7ba4aed..fb913bc 100644 --- a/src/client/web/src/components/state_mgr.tsx +++ b/src/client/web/src/components/state_mgr.tsx @@ -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 { }) .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 { update={this.update} browser={this.state.panel.browser} panes={this.state.panel.panes} - admin={this.state.panel.admin} /> ); } diff --git a/src/handlers/fileshdr/handlers.go b/src/handlers/fileshdr/handlers.go index 68331fe..6044183 100644 --- a/src/handlers/fileshdr/handlers.go +++ b/src/handlers/fileshdr/handlers.go @@ -118,7 +118,7 @@ func (h *FileHandlers) Create(c *gin.Context) { return } - tmpFilePath := q.GetTmpPath(userID, req.Path) + tmpFilePath := q.UploadPath(userID, req.Path) locker := h.NewAutoLocker(c, lockName(tmpFilePath)) locker.Exec(func() { err := h.deps.FS().Create(tmpFilePath) @@ -295,7 +295,7 @@ func (h *FileHandlers) UploadChunk(c *gin.Context) { return } - tmpFilePath := q.GetTmpPath(userID, req.Path) + tmpFilePath := q.UploadPath(userID, req.Path) locker := h.NewAutoLocker(c, lockName(tmpFilePath)) locker.Exec(func() { var err error @@ -407,7 +407,7 @@ func (h *FileHandlers) UploadStatus(c *gin.Context) { return } - tmpFilePath := q.GetTmpPath(userID, filePath) + tmpFilePath := q.UploadPath(userID, filePath) locker := h.NewAutoLocker(c, lockName(tmpFilePath)) locker.Exec(func() { _, fileSize, uploaded, err := h.uploadMgr.GetInfo(userID, tmpFilePath) @@ -549,7 +549,8 @@ func (h *FileHandlers) List(c *gin.Context) { func (h *FileHandlers) ListHome(c *gin.Context) { userID := c.MustGet(q.UserIDParam).(string) - infos, err := h.deps.FS().ListDir(userID) + fsPath := q.FsRootPath(userID, "/") + infos, err := h.deps.FS().ListDir(fsPath) if err != nil { c.JSON(q.ErrResp(c, 500, err)) return @@ -565,7 +566,7 @@ func (h *FileHandlers) ListHome(c *gin.Context) { } c.JSON(200, &ListResp{ - Cwd: userID, + Cwd: fsPath, Metadatas: metadatas, }) } @@ -606,7 +607,7 @@ func (h *FileHandlers) DelUploading(c *gin.Context) { userID := c.MustGet(q.UserIDParam).(string) var err error - tmpFilePath := q.GetTmpPath(userID, filePath) + tmpFilePath := q.UploadPath(userID, filePath) locker := h.NewAutoLocker(c, lockName(tmpFilePath)) locker.Exec(func() { err = h.deps.FS().Remove(tmpFilePath) diff --git a/src/handlers/fileshdr/upload_mgr.go b/src/handlers/fileshdr/upload_mgr.go index f1a5b77..9326c2a 100644 --- a/src/handlers/fileshdr/upload_mgr.go +++ b/src/handlers/fileshdr/upload_mgr.go @@ -102,7 +102,12 @@ func (um *UploadMgr) DelInfo(user, filePath string) error { } func (um *UploadMgr) ListInfo(user string) ([]*UploadInfo, error) { - infoMap, err := um.kv.ListStringsIn(UploadNS(user)) + ns := UploadNS(user) + if !um.kv.HasNamespace(ns) { + return nil, nil + } + + infoMap, err := um.kv.ListStringsIn(ns) if err != nil { return nil, err } diff --git a/src/handlers/multiusers/handlers.go b/src/handlers/multiusers/handlers.go index 01e1577..18436f6 100644 --- a/src/handlers/multiusers/handlers.go +++ b/src/handlers/multiusers/handlers.go @@ -33,36 +33,41 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error) apiACRules := map[string]bool{ // TODO: make these configurable // admin rules - apiRuleCname(userstore.AdminRole, "GET", "/"): true, - apiRuleCname(userstore.AdminRole, "GET", publicPath): true, - apiRuleCname(userstore.AdminRole, "POST", "/v1/users/login"): true, - apiRuleCname(userstore.AdminRole, "POST", "/v1/users/logout"): true, - apiRuleCname(userstore.AdminRole, "GET", "/v1/users/isauthed"): true, - apiRuleCname(userstore.AdminRole, "PATCH", "/v1/users/pwd"): true, - apiRuleCname(userstore.AdminRole, "POST", "/v1/users/"): true, - apiRuleCname(userstore.AdminRole, "POST", "/v1/roles/"): true, - apiRuleCname(userstore.AdminRole, "DELETE", "/v1/roles/"): true, - apiRuleCname(userstore.AdminRole, "GET", "/v1/roles/"): true, - apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/files"): true, - apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/files"): true, - apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files"): true, - apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/chunks"): true, - apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files/chunks"): true, - apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/copy"): true, - apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/move"): true, - apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/dirs"): true, - apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/dirs/home"): true, - apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/dirs"): true, - apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/uploadings"): true, - apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/uploadings"): true, - apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/metadata"): true, - apiRuleCname(userstore.AdminRole, "OPTIONS", "/v1/settings/health"): true, + apiRuleCname(userstore.AdminRole, "GET", "/"): true, + apiRuleCname(userstore.AdminRole, "GET", publicPath): true, + apiRuleCname(userstore.AdminRole, "POST", "/v1/users/login"): true, + apiRuleCname(userstore.AdminRole, "POST", "/v1/users/logout"): true, + apiRuleCname(userstore.AdminRole, "GET", "/v1/users/isauthed"): true, + apiRuleCname(userstore.AdminRole, "PATCH", "/v1/users/pwd"): true, + apiRuleCname(userstore.AdminRole, "PATCH", "/v1/users/pwd/force-set"): true, + apiRuleCname(userstore.AdminRole, "POST", "/v1/users/"): true, + apiRuleCname(userstore.AdminRole, "DELETE", "/v1/users/"): true, + apiRuleCname(userstore.AdminRole, "GET", "/v1/users/list"): true, + apiRuleCname(userstore.AdminRole, "GET", "/v1/users/self"): true, + apiRuleCname(userstore.AdminRole, "POST", "/v1/roles/"): true, + apiRuleCname(userstore.AdminRole, "DELETE", "/v1/roles/"): true, + apiRuleCname(userstore.AdminRole, "GET", "/v1/roles/list"): true, + apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/files"): true, + apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/files"): true, + apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files"): true, + apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/chunks"): true, + apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/files/chunks"): true, + apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/copy"): true, + apiRuleCname(userstore.AdminRole, "PATCH", "/v1/fs/files/move"): true, + apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/dirs"): true, + apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/dirs/home"): true, + apiRuleCname(userstore.AdminRole, "POST", "/v1/fs/dirs"): true, + apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/uploadings"): true, + apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/uploadings"): true, + apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/metadata"): true, + apiRuleCname(userstore.AdminRole, "OPTIONS", "/v1/settings/health"): true, // user rules apiRuleCname(userstore.UserRole, "GET", "/"): true, apiRuleCname(userstore.UserRole, "GET", publicPath): true, apiRuleCname(userstore.UserRole, "POST", "/v1/users/logout"): true, apiRuleCname(userstore.UserRole, "GET", "/v1/users/isauthed"): true, apiRuleCname(userstore.UserRole, "PATCH", "/v1/users/pwd"): true, + apiRuleCname(userstore.UserRole, "GET", "/v1/users/self"): true, apiRuleCname(userstore.UserRole, "POST", "/v1/fs/files"): true, apiRuleCname(userstore.UserRole, "DELETE", "/v1/fs/files"): true, apiRuleCname(userstore.UserRole, "GET", "/v1/fs/files"): true, @@ -82,6 +87,7 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error) apiRuleCname(userstore.VisitorRole, "GET", publicPath): true, apiRuleCname(userstore.VisitorRole, "POST", "/v1/users/login"): true, apiRuleCname(userstore.VisitorRole, "GET", "/v1/users/isauthed"): true, + apiRuleCname(userstore.VisitorRole, "GET", "/v1/users/self"): true, apiRuleCname(userstore.VisitorRole, "GET", "/v1/fs/files"): true, apiRuleCname(userstore.VisitorRole, "OPTIONS", "/v1/settings/health"): true, } @@ -97,12 +103,12 @@ func (h *MultiUsersSvc) Init(adminName, adminPwd string) (string, error) { var err error userID := "0" - fsPath := q.HomePath(userID, "/") + fsPath := q.FsRootPath(userID, "/") if err = h.deps.FS().MkdirAll(fsPath); err != nil { return "", err } - uploadingsPath := q.GetTmpPath(userID, "/") - if err = h.deps.FS().MkdirAll(uploadingsPath); err != nil { + uploadFolder := q.UploadFolder(userID) + if err = h.deps.FS().MkdirAll(uploadFolder); err != nil { return "", err } @@ -231,6 +237,57 @@ func (h *MultiUsersSvc) SetPwd(c *gin.Context) { c.JSON(q.Resp(200)) } +type ForceSetPwdReq struct { + ID string `json:"id"` + NewPwd string `json:"newPwd"` +} + +func (h *MultiUsersSvc) ForceSetPwd(c *gin.Context) { + req := &ForceSetPwdReq{} + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(q.ErrResp(c, 400, err)) + return + } + + claims, err := h.getUserInfo(c) + if err != nil { + c.JSON(q.ErrResp(c, 401, err)) + return + } + if claims[q.RoleParam] != userstore.AdminRole { + c.JSON(q.ErrResp(c, 403, errors.New("operation denied"))) + return + } + targetUID, err := strconv.ParseUint(req.ID, 10, 64) + if err != nil { + c.JSON(q.ErrResp(c, 500, err)) + return + } + targetUser, err := h.deps.Users().GetUser(targetUID) + if err != nil { + c.JSON(q.ErrResp(c, 500, err)) + return + } + if targetUser.Role == userstore.AdminRole { + c.JSON(q.ErrResp(c, 403, errors.New("can not set admin's password"))) + return + } + + newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPwd), 10) + if err != nil { + c.JSON(q.ErrResp(c, 500, errors.New("fail to set password"))) + return + } + + err = h.deps.Users().SetPwd(targetUser.ID, string(newHash)) + if err != nil { + c.JSON(q.ErrResp(c, 500, err)) + return + } + + c.JSON(q.Resp(200)) +} + type AddUserReq struct { Name string `json:"name"` Pwd string `json:"pwd"` @@ -267,14 +324,14 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) { // TODO: following operations must be atomic // TODO: check if the folders already exists - userID := c.MustGet(q.UserIDParam).(string) - homePath := q.HomePath(userID, "/") - if err = h.deps.FS().MkdirAll(homePath); err != nil { + uidStr := fmt.Sprint(uid) + fsRootFolder := q.FsRootPath(uidStr, "/") + if err = h.deps.FS().MkdirAll(fsRootFolder); err != nil { c.JSON(q.ErrResp(c, 500, err)) return } - uploadingsPath := q.GetTmpPath(userID, "/") - if err = h.deps.FS().MkdirAll(uploadingsPath); err != nil { + uploadFolder := q.UploadFolder(uidStr) + if err = h.deps.FS().MkdirAll(uploadFolder); err != nil { c.JSON(q.ErrResp(c, 500, err)) return } @@ -293,6 +350,71 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) { c.JSON(200, &AddUserResp{ID: fmt.Sprint(uid)}) } +type DelUserResp struct { + ID string `json:"id"` +} + +func (h *MultiUsersSvc) DelUser(c *gin.Context) { + userIDStr := c.Query(q.UserIDParam) + userID, err := strconv.ParseUint(userIDStr, 10, 64) + if err != nil { + c.JSON(q.ErrResp(c, 400, fmt.Errorf("invalid users ID %w", err))) + return + } else if userID == 0 { + c.JSON(q.ErrResp(c, 400, errors.New("It is not allowed to delete root"))) + return + } + + claims, err := h.getUserInfo(c) + if err != nil { + c.JSON(q.ErrResp(c, 401, err)) + return + } + if claims[q.UserIDParam] == userIDStr { + c.JSON(q.ErrResp(c, 403, errors.New("can not delete self"))) + return + } + + // TODO: try to make following atomic + err = h.deps.Users().DelUser(userID) + if err != nil { + c.JSON(q.ErrResp(c, 500, err)) + return + } + + // TODO: move the folder to recycle bin when it failed to remove it + homePath := userIDStr + if err = h.deps.FS().Remove(homePath); err != nil { + c.JSON(q.ErrResp(c, 500, err)) + return + } + c.JSON(200, &DelUserResp{ID: userIDStr}) +} + +type ListUsersResp struct { + Users []*userstore.User `json:"users"` +} + +func (h *MultiUsersSvc) ListUsers(c *gin.Context) { + // TODO: pagination is not enabled + // lastID := 0 + // lastIDStr := c.Query(q.LastID) + // if lastIDStr != "" { + // lastID, err := strconv.Atoi(lastIDStr) + // if err != nil { + // c.JSON(q.ErrResp(c, 400, fmt.Errorf("invalid param %w", err))) + // return + // } + // } + + users, err := h.deps.Users().ListUsers() + if err != nil { + c.JSON(q.ErrResp(c, 500, err)) + return + } + c.JSON(200, &ListUsersResp{Users: users}) +} + type AddRoleReq struct { Role string `json:"role"` } @@ -405,3 +527,23 @@ func (h *MultiUsersSvc) isValidRole(role string) error { } return h.isValidUserName(role) } + +type SelfResp struct { + ID string `json:"id"` + Name string `json:"name"` + Role string `json:"role"` +} + +func (h *MultiUsersSvc) Self(c *gin.Context) { + claims, err := h.getUserInfo(c) + if err != nil { + c.JSON(q.ErrResp(c, 401, err)) + return + } + + c.JSON(200, &SelfResp{ + ID: claims[q.UserIDParam], + Name: claims[q.UserParam], + Role: claims[q.RoleParam], + }) +} diff --git a/src/handlers/util.go b/src/handlers/util.go index 4b456b3..aedffcd 100644 --- a/src/handlers/util.go +++ b/src/handlers/util.go @@ -13,6 +13,7 @@ var ( // dirs UploadDir = "uploadings" FsDir = "files" + FsRootDir = "files" UserIDParam = "uid" UserParam = "user" @@ -21,6 +22,7 @@ var ( RoleParam = "role" ExpireParam = "expire" TokenCookie = "tk" + LastID = "lid" ErrAccessDenied = errors.New("access denied") ErrUnauthorized = errors.New("unauthorized") @@ -131,6 +133,18 @@ func HomePath(userID, relFilePath string) string { return filepath.Join(userID, relFilePath) } +func FsRootPath(userID, relFilePath string) string { + return filepath.Join(userID, FsRootDir, relFilePath) +} + func GetTmpPath(userID, relFilePath string) string { return filepath.Join(UploadDir, userID, fmt.Sprintf("%x", sha1.Sum([]byte(relFilePath)))) } + +func UploadPath(userID, relFilePath string) string { + return filepath.Join(UploadFolder(userID), fmt.Sprintf("%x", sha1.Sum([]byte(relFilePath)))) +} + +func UploadFolder(userID string) string { + return filepath.Join(userID, UploadDir) +} diff --git a/src/kvstore/boltdbpvd/provider.go b/src/kvstore/boltdbpvd/provider.go index f1c4a46..671ca12 100644 --- a/src/kvstore/boltdbpvd/provider.go +++ b/src/kvstore/boltdbpvd/provider.go @@ -72,6 +72,18 @@ func (bp *BoltPvd) DelNamespace(nsName string) error { }) } +func (bp *BoltPvd) HasNamespace(nsName string) bool { + err := bp.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(nsName)) + if b == nil { + return ErrBucketNotFound + } + return nil + }) + + return err == nil +} + func (bp *BoltPvd) Close() error { return bp.db.Close() } diff --git a/src/kvstore/kvstore_interface.go b/src/kvstore/kvstore_interface.go index 212b384..ddbcfce 100644 --- a/src/kvstore/kvstore_interface.go +++ b/src/kvstore/kvstore_interface.go @@ -8,6 +8,7 @@ var ErrNoLock = errors.New("no lock to unlock") type IKVStore interface { AddNamespace(nsName string) error DelNamespace(nsName string) error + HasNamespace(nsName string) bool GetBool(key string) (bool, bool) GetBoolIn(ns, key string) (bool, bool) SetBool(key string, val bool) error diff --git a/src/server/server.go b/src/server/server.go index bcb5066..58b2593 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -178,12 +178,16 @@ func initHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.E usersAPI.POST("/logout", userHdrs.Logout) usersAPI.GET("/isauthed", userHdrs.IsAuthed) usersAPI.PATCH("/pwd", userHdrs.SetPwd) + usersAPI.PATCH("/pwd/force-set", userHdrs.ForceSetPwd) usersAPI.POST("/", userHdrs.AddUser) + usersAPI.DELETE("/", userHdrs.DelUser) + usersAPI.GET("/list", userHdrs.ListUsers) + usersAPI.GET("/self", userHdrs.Self) rolesAPI := v1.Group("/roles") rolesAPI.POST("/", userHdrs.AddRole) rolesAPI.DELETE("/", userHdrs.DelRole) - rolesAPI.GET("/", userHdrs.ListRoles) + rolesAPI.GET("/list", userHdrs.ListRoles) filesAPI := v1.Group("/fs") filesAPI.POST("/files", fileHdrs.Create) diff --git a/src/server/server_concurrency_test.go b/src/server/server_concurrency_test.go new file mode 100644 index 0000000..c256b01 --- /dev/null +++ b/src/server/server_concurrency_test.go @@ -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) + } +} diff --git a/src/server/server_files_test.go b/src/server/server_files_test.go index 0540194..8c3debf 100644 --- a/src/server/server_files_test.go +++ b/src/server/server_files_test.go @@ -1,15 +1,11 @@ package server import ( - "crypto/sha1" "encoding/base64" "fmt" "math/rand" - "net/http" "os" - "path" "path/filepath" - "strings" "sync" "testing" @@ -65,120 +61,67 @@ func TestFileHandlers(t *testing.T) { token := client.GetCookie(resp.Cookies(), q.TokenCookie) cl := client.NewFilesClient(addr, token) - assertUploadOK := func(t *testing.T, filePath, content string) bool { - cl := client.NewFilesClient(addr, token) + // TODO: remove all files under home folder before testing + // or the count of files is incorrect + t.Run("ListHome", func(t *testing.T) { + files := map[string]string{ + "0/files/home_file1": "12345678", + "0/files/home_file2": "12345678", + } - fileSize := int64(len([]byte(content))) - res, _, errs := cl.Create(filePath, fileSize) + for filePath, content := range files { + assertUploadOK(t, filePath, content, addr, token) + + err = fs.Sync() + if err != nil { + t.Fatal(err) + } + } + + resp, lhResp, errs := cl.ListHome() if len(errs) > 0 { - t.Error(errs) - return false - } else if res.StatusCode != 200 { - t.Error(res.StatusCode) - return false - } - - base64Content := base64.StdEncoding.EncodeToString([]byte(content)) - res, _, errs = cl.UploadChunk(filePath, base64Content, 0) - if len(errs) > 0 { - t.Error(errs) - return false - } else if res.StatusCode != 200 { - t.Error(res.StatusCode) - return false - } - - return true - } - - assetDownloadOK := func(t *testing.T, filePath, content string) bool { - var ( - res *http.Response - body string - errs []error - fileSize = int64(len([]byte(content))) - ) - - // cl := client.NewFilesClient(addr) - - rd := rand.Intn(3) - switch rd { - case 0: - res, body, errs = cl.Download(filePath, map[string]string{}) - case 1: - res, body, errs = cl.Download(filePath, map[string]string{ - "Range": fmt.Sprintf("bytes=0-%d", fileSize-1), - }) - case 2: - res, body, errs = cl.Download(filePath, map[string]string{ - "Range": fmt.Sprintf("bytes=0-%d, %d-%d", (fileSize-1)/2, (fileSize-1)/2+1, fileSize-1), - }) - } - - fileName := path.Base(filePath) - contentDispositionHeader := res.Header.Get("Content-Disposition") - if len(errs) > 0 { - t.Error(errs) - return false - } - if res.StatusCode != 200 && res.StatusCode != 206 { - t.Error(res.StatusCode) - return false - } - if contentDispositionHeader != fmt.Sprintf(`attachment; filename="%s"`, fileName) { - t.Errorf("incorrect Content-Disposition header: %s", contentDispositionHeader) - return false - } - - switch rd { - case 0: - if body != content { - t.Errorf("body not equal got(%s) expect(%s)\n", body, content) - return false - } - case 1: - if body[2:] != content { // body returned by gorequest contains the first CRLF - t.Errorf("body not equal got(%s) expect(%s)\n", body[2:], content) - return false - } - default: - body = body[2:] // body returned by gorequest contains the first CRLF - realBody := "" - boundaryEnd := strings.Index(body, "\r\n") - boundary := body[0:boundaryEnd] - bodyParts := strings.Split(body, boundary) - - for i, bodyPart := range bodyParts { - if i == 0 || i == len(bodyParts)-1 { - continue - } - start := strings.Index(bodyPart, "\r\n\r\n") - - fmt.Printf("<%s>", bodyPart[start+4:len(bodyPart)-2]) // ignore the last CRLF - realBody += bodyPart[start+4 : len(bodyPart)-2] - } - if realBody != content { - t.Errorf("multi body not equal got(%s) expect(%s)\n", realBody, content) - return false + t.Fatal(errs) + } else if resp.StatusCode != 200 { + t.Fatal(resp.StatusCode) + } else if lhResp.Cwd != "0/files" { + t.Fatalf("incorrect ListHome cwd %s", lhResp.Cwd) + } else if len(lhResp.Metadatas) != len(files) { + for _, metadata := range lhResp.Metadatas { + fmt.Printf("%v\n", metadata) } + t.Fatalf("incorrect ListHome content %d", len(lhResp.Metadatas)) } - return true - } + infos := map[string]*fileshdr.MetadataResp{} + for _, metadata := range lhResp.Metadatas { + infos[metadata.Name] = metadata + } + + if infos["home_file1"].Size != int64(len(files["0/files/home_file1"])) { + t.Fatalf("incorrect file size %d", infos["home_file1"].Size) + } else if infos["home_file1"].IsDir { + t.Fatal("incorrect item type") + } + if infos["home_file2"].Size != int64(len(files["0/files/home_file2"])) { + t.Fatalf("incorrect file size %d", infos["home_file2"].Size) + } else if infos["home_file2"].IsDir { + t.Fatal("incorrect item type") + } + }) t.Run("test uploading files with duplicated names", func(t *testing.T) { files := map[string]string{ - "0/dupdir/dup_file1": "12345678", - "0/dupdir/dup_file2.ext": "12345678", + "0/files/dupdir/dup_file1": "12345678", + "0/files/dupdir/dup_file2.ext": "12345678", } renames := map[string]string{ - "0/dupdir/dup_file1": "0/dupdir/dup_file1_1", - "0/dupdir/dup_file2.ext": "0/dupdir/dup_file2_1.ext", + "0/files/dupdir/dup_file1": "0/files/dupdir/dup_file1_1", + "0/files/dupdir/dup_file2.ext": "0/files/dupdir/dup_file2_1.ext", } for filePath, content := range files { for i := 0; i < 2; i++ { - assertUploadOK(t, filePath, content) + assertUploadOK(t, filePath, content, addr, token) err = fs.Sync() if err != nil { @@ -186,13 +129,13 @@ func TestFileHandlers(t *testing.T) { } if i == 0 { - assetDownloadOK(t, filePath, content) + assertDownloadOK(t, filePath, content, addr, token) } else if i == 1 { renamedFilePath, ok := renames[filePath] if !ok { t.Fatal("new name not found") } - assetDownloadOK(t, renamedFilePath, content) + assertDownloadOK(t, renamedFilePath, content, addr, token) } } } @@ -200,8 +143,8 @@ func TestFileHandlers(t *testing.T) { t.Run("test files APIs: Create-UploadChunk-UploadStatus-Metadata-Delete", func(t *testing.T) { for filePath, content := range map[string]string{ - "0/path1/f1.md": "1111 1111 1111 1111", - "0/path1/path2/f2.md": "1010 1010 1111 0000 0010", + "0/files/path1/f1.md": "1111 1111 1111 1111", + "0/files/path1/path2/f2.md": "1010 1010 1111 0000 0010", } { fileSize := int64(len([]byte(content))) // create a file @@ -213,7 +156,7 @@ func TestFileHandlers(t *testing.T) { } // check uploading file - uploadFilePath := path.Join(q.UploadDir, "0", fmt.Sprintf("%x", sha1.Sum([]byte(filePath)))) + uploadFilePath := q.UploadPath("0", filePath) info, err := fs.Stat(uploadFilePath) if err != nil { t.Fatal(err) @@ -290,11 +233,11 @@ func TestFileHandlers(t *testing.T) { t.Run("test dirs APIs: Mkdir-Create-UploadChunk-List", func(t *testing.T) { for dirPath, files := range map[string]map[string]string{ - "0/dir/path1": map[string]string{ + "0/files/dir/path1": map[string]string{ "f1.md": "11111", "f2.md": "22222222222", }, - "0/dir/path2/path2": map[string]string{ + "0/files/dir/path2/path2": map[string]string{ "f3.md": "3333333", }, } { @@ -307,7 +250,7 @@ func TestFileHandlers(t *testing.T) { for fileName, content := range files { filePath := filepath.Join(dirPath, fileName) - assertUploadOK(t, filePath, content) + assertUploadOK(t, filePath, content, addr, token) } err = fs.Sync() @@ -331,8 +274,8 @@ func TestFileHandlers(t *testing.T) { }) t.Run("test operation APIs: Mkdir-Create-UploadChunk-Move-List", func(t *testing.T) { - srcDir := "0/move/src" - dstDir := "0/move/dst" + srcDir := "0/files/move/src" + dstDir := "0/files/move/dst" for _, dirPath := range []string{srcDir, dstDir} { res, _, errs := cl.Mkdir(dirPath) @@ -352,7 +295,7 @@ func TestFileHandlers(t *testing.T) { oldPath := filepath.Join(srcDir, fileName) newPath := filepath.Join(dstDir, fileName) // fileSize := int64(len([]byte(content))) - assertUploadOK(t, oldPath, content) + assertUploadOK(t, oldPath, content, addr, token) res, _, errs := cl.Move(oldPath, newPath) if len(errs) > 0 { @@ -383,17 +326,17 @@ func TestFileHandlers(t *testing.T) { t.Run("test download APIs: Download(normal, ranges)", func(t *testing.T) { for filePath, content := range map[string]string{ - "0/download/path1/f1": "123456", - "0/download/path1/path2": "12345678", + "0/files/download/path1/f1": "123456", + "0/files/download/path1/path2": "12345678", } { - assertUploadOK(t, filePath, content) + assertUploadOK(t, filePath, content, addr, token) err = fs.Sync() if err != nil { t.Fatal(err) } - assetDownloadOK(t, filePath, content) + assertDownloadOK(t, filePath, content, addr, token) } }) @@ -407,7 +350,7 @@ func TestFileHandlers(t *testing.T) { startClient := func(files []*mockFile) { for i := 0; i < 5; i++ { for _, file := range files { - if !assertUploadOK(t, fmt.Sprintf("%s_%d", file.FilePath, i), file.Content) { + if !assertUploadOK(t, fmt.Sprintf("%s_%d", file.FilePath, i), file.Content, addr, token) { break } @@ -416,7 +359,7 @@ func TestFileHandlers(t *testing.T) { t.Fatal(err) } - if !assetDownloadOK(t, fmt.Sprintf("%s_%d", file.FilePath, i), file.Content) { + if !assertDownloadOK(t, fmt.Sprintf("%s_%d", file.FilePath, i), file.Content, addr, token) { break } } @@ -446,10 +389,18 @@ func TestFileHandlers(t *testing.T) { wg.Wait() }) - t.Run("test uploading APIs: Create, ListUploadings, DelUploading", func(t *testing.T) { + t.Run("test uploading APIs: ListUploadings, Create, ListUploadings, DelUploading", func(t *testing.T) { + // it should return no error even no file is uploaded + res, lResp, errs := cl.ListUploadings() + if len(errs) > 0 { + t.Fatal(errs) + } else if res.StatusCode != 200 { + t.Fatal(res.StatusCode) + } + files := map[string]string{ - "0/uploadings/path1/f1": "123456", - "0/uploadings/path1/path2": "12345678", + "0/files/uploadings/path1/f1": "123456", + "0/files/uploadings/path1/path2": "12345678", } for filePath, content := range files { @@ -462,7 +413,7 @@ func TestFileHandlers(t *testing.T) { } } - res, lResp, errs := cl.ListUploadings() + res, lResp, errs = cl.ListUploadings() if len(errs) > 0 { t.Fatal(errs) } else if res.StatusCode != 200 { @@ -507,7 +458,7 @@ func TestFileHandlers(t *testing.T) { // cl := client.NewFilesClient(addr) files := map[string]string{ - "0/uploadings/path1/f1": "12345678", + "0/files/uploadings/path1/f1": "12345678", } for filePath, content := range files { @@ -554,7 +505,7 @@ func TestFileHandlers(t *testing.T) { t.Fatal("incorrect uploaded size", mRes) } - assetDownloadOK(t, filePath, content) + assertDownloadOK(t, filePath, content, addr, token) } }) @@ -562,9 +513,9 @@ func TestFileHandlers(t *testing.T) { // cl := client.NewFilesClient(addr) files := map[string]string{ - "0/uploadings/random/path1/f1": "12345678", - "0/uploadings/random/path1/f2": "87654321", - "0/uploadings/random/path1/f3": "17654321", + "0/files/uploadings/random/path1/f1": "12345678", + "0/files/uploadings/random/path1/f2": "87654321", + "0/files/uploadings/random/path1/f3": "17654321", } for filePath, content := range files { @@ -622,7 +573,7 @@ func TestFileHandlers(t *testing.T) { t.Fatalf("file content not equal: %s", filePath) } - assetDownloadOK(t, filePath, content) + assertDownloadOK(t, filePath, content, addr, token) } }) diff --git a/src/server/server_users_test.go b/src/server/server_users_test.go index 9110411..bb41cdf 100644 --- a/src/server/server_users_test.go +++ b/src/server/server_users_test.go @@ -3,14 +3,16 @@ package server import ( "fmt" "os" + "strconv" "testing" "github.com/ihexxa/quickshare/src/client" + q "github.com/ihexxa/quickshare/src/handlers" su "github.com/ihexxa/quickshare/src/handlers/singleuserhdr" "github.com/ihexxa/quickshare/src/userstore" ) -func TestSingleUserHandlers(t *testing.T) { +func TestUsersHandlers(t *testing.T) { addr := "http://127.0.0.1:8686" root := "testData" config := `{ @@ -41,6 +43,7 @@ func TestSingleUserHandlers(t *testing.T) { srv := startTestServer(config) defer srv.Shutdown() + fs := srv.depsFS() usersCl := client.NewSingleUserClient(addr) @@ -48,7 +51,7 @@ func TestSingleUserHandlers(t *testing.T) { t.Fatal("fail to start server") } - t.Run("test users APIs: Login-SetPwd-Logout-Login", func(t *testing.T) { + t.Run("test users APIs: Login-Self-SetPwd-Logout-Login", func(t *testing.T) { resp, _, errs := usersCl.Login(adminName, adminPwd) if len(errs) > 0 { t.Fatal(errs) @@ -58,6 +61,17 @@ func TestSingleUserHandlers(t *testing.T) { token := client.GetCookie(resp.Cookies(), su.TokenCookie) + resp, selfResp, errs := usersCl.Self(token) + if len(errs) > 0 { + t.Fatal(errs) + } else if resp.StatusCode != 200 { + t.Fatal(resp.StatusCode) + } else if selfResp.ID != "0" || + selfResp.Name != adminName || + selfResp.Role != userstore.AdminRole { + t.Fatalf("user infos don't match %v", selfResp) + } + resp, _, errs = usersCl.SetPwd(adminPwd, adminNewPwd, token) if len(errs) > 0 { t.Fatal(errs) @@ -90,7 +104,7 @@ func TestSingleUserHandlers(t *testing.T) { token := client.GetCookie(resp.Cookies(), su.TokenCookie) - userName, userPwd := "user", "1234" + userName, userPwd := "user_login", "1234" resp, auResp, errs := usersCl.AddUser(userName, userPwd, userstore.UserRole, token) if len(errs) > 0 { t.Fatal(errs) @@ -100,6 +114,18 @@ func TestSingleUserHandlers(t *testing.T) { // TODO: check id fmt.Printf("new user id: %v\n", auResp) + // check uploading file + userFsRootFolder := q.FsRootPath(auResp.ID, "/") + _, err = fs.Stat(userFsRootFolder) + if err != nil { + t.Fatal(err) + } + userUploadFolder := q.UploadFolder(auResp.ID) + _, err = fs.Stat(userUploadFolder) + if err != nil { + t.Fatal(err) + } + resp, _, errs = usersCl.Logout(token) if len(errs) > 0 { t.Fatal(errs) @@ -114,6 +140,85 @@ func TestSingleUserHandlers(t *testing.T) { t.Fatal(resp.StatusCode) } + resp, _, errs = usersCl.DelUser(auResp.ID, token) + if len(errs) > 0 { + t.Fatal(errs) + } else if resp.StatusCode != 200 { + t.Fatal(resp.StatusCode) + } + + resp, _, errs = usersCl.Logout(token) + if len(errs) > 0 { + t.Fatal(errs) + } else if resp.StatusCode != 200 { + t.Fatal(resp.StatusCode) + } + }) + + t.Run("test users APIs: Login-AddUser-ListUsers-DelUser-ListUsers", func(t *testing.T) { + resp, _, errs := usersCl.Login(adminName, adminNewPwd) + if len(errs) > 0 { + t.Fatal(errs) + } else if resp.StatusCode != 200 { + t.Fatal(resp.StatusCode) + } + + token := client.GetCookie(resp.Cookies(), su.TokenCookie) + + userName, userPwd, userRole := "user_admin", "1234", userstore.UserRole + resp, auResp, errs := usersCl.AddUser(userName, userPwd, userRole, token) + if len(errs) > 0 { + t.Fatal(errs) + } else if resp.StatusCode != 200 { + t.Fatal(resp.StatusCode) + } + // TODO: check id + fmt.Printf("new user id: %v\n", auResp) + newUserID, err := strconv.ParseUint(auResp.ID, 10, 64) + if err != nil { + t.Fatal(err) + } + + resp, lsResp, errs := usersCl.ListUsers(token) + if len(errs) > 0 { + t.Fatal(errs) + } else if resp.StatusCode != 200 { + t.Fatal(resp.StatusCode) + } + + if len(lsResp.Users) != 2 { + t.Fatal(fmt.Errorf("incorrect users size (%d)", len(lsResp.Users))) + } else if lsResp.Users[0].ID != 0 || + lsResp.Users[0].Name != adminName || + lsResp.Users[0].Role != userstore.AdminRole { + t.Fatal(fmt.Errorf("incorrect root info (%v)", lsResp.Users[0])) + } else if lsResp.Users[1].ID != newUserID || + lsResp.Users[1].Name != userName || + lsResp.Users[1].Role != userRole { + t.Fatal(fmt.Errorf("incorrect user info (%v)", lsResp.Users[1])) + } + + resp, _, errs = usersCl.DelUser(auResp.ID, token) + if len(errs) > 0 { + t.Fatal(errs) + } else if resp.StatusCode != 200 { + t.Fatal(resp.StatusCode) + } + + resp, lsResp, errs = usersCl.ListUsers(token) + if len(errs) > 0 { + t.Fatal(errs) + } else if resp.StatusCode != 200 { + t.Fatal(resp.StatusCode) + } + if len(lsResp.Users) != 1 { + t.Fatal(fmt.Errorf("incorrect users size (%d)", len(lsResp.Users))) + } else if lsResp.Users[0].ID != 0 || + lsResp.Users[0].Name != adminName || + lsResp.Users[0].Role != userstore.AdminRole { + t.Fatal(fmt.Errorf("incorrect root info (%v)", lsResp.Users[0])) + } + resp, _, errs = usersCl.Logout(token) if len(errs) > 0 { t.Fatal(errs) diff --git a/src/server/test_helpers.go b/src/server/test_helpers.go index 21c4a2b..f44dfcc 100644 --- a/src/server/test_helpers.go +++ b/src/server/test_helpers.go @@ -1,8 +1,14 @@ package server import ( + "encoding/base64" + "fmt" "io/ioutil" - // "path" + "math/rand" + "net/http" + "path" + "strings" + "testing" "time" "github.com/ihexxa/gocfg" @@ -64,3 +70,104 @@ func compareFileContent(fs fspkg.ISimpleFS, uid, filePath string, expectedConten return string(gotContent) == expectedContent, nil } + +func assertUploadOK(t *testing.T, filePath, content, addr string, token *http.Cookie) bool { + cl := client.NewFilesClient(addr, token) + + fileSize := int64(len([]byte(content))) + res, _, errs := cl.Create(filePath, fileSize) + if len(errs) > 0 { + t.Error(errs) + return false + } else if res.StatusCode != 200 { + t.Error(res.StatusCode) + return false + } + + base64Content := base64.StdEncoding.EncodeToString([]byte(content)) + res, _, errs = cl.UploadChunk(filePath, base64Content, 0) + if len(errs) > 0 { + t.Error(errs) + return false + } else if res.StatusCode != 200 { + t.Error(res.StatusCode) + return false + } + + return true +} + +func assertDownloadOK(t *testing.T, filePath, content, addr string, token *http.Cookie) bool { + var ( + res *http.Response + body string + errs []error + fileSize = int64(len([]byte(content))) + ) + + cl := client.NewFilesClient(addr, token) + + rd := rand.Intn(3) + switch rd { + case 0: + res, body, errs = cl.Download(filePath, map[string]string{}) + case 1: + res, body, errs = cl.Download(filePath, map[string]string{ + "Range": fmt.Sprintf("bytes=0-%d", fileSize-1), + }) + case 2: + res, body, errs = cl.Download(filePath, map[string]string{ + "Range": fmt.Sprintf("bytes=0-%d, %d-%d", (fileSize-1)/2, (fileSize-1)/2+1, fileSize-1), + }) + } + + fileName := path.Base(filePath) + contentDispositionHeader := res.Header.Get("Content-Disposition") + if len(errs) > 0 { + t.Error(errs) + return false + } + if res.StatusCode != 200 && res.StatusCode != 206 { + t.Error(res.StatusCode) + return false + } + if contentDispositionHeader != fmt.Sprintf(`attachment; filename="%s"`, fileName) { + t.Errorf("incorrect Content-Disposition header: %s", contentDispositionHeader) + return false + } + + switch rd { + case 0: + if body != content { + t.Errorf("body not equal got(%s) expect(%s)\n", body, content) + return false + } + case 1: + if body[2:] != content { // body returned by gorequest contains the first CRLF + t.Errorf("body not equal got(%s) expect(%s)\n", body[2:], content) + return false + } + default: + body = body[2:] // body returned by gorequest contains the first CRLF + realBody := "" + boundaryEnd := strings.Index(body, "\r\n") + boundary := body[0:boundaryEnd] + bodyParts := strings.Split(body, boundary) + + for i, bodyPart := range bodyParts { + if i == 0 || i == len(bodyParts)-1 { + continue + } + start := strings.Index(bodyPart, "\r\n\r\n") + + fmt.Printf("<%s>", bodyPart[start+4:len(bodyPart)-2]) // ignore the last CRLF + realBody += bodyPart[start+4 : len(bodyPart)-2] + } + if realBody != content { + t.Errorf("multi body not equal got(%s) expect(%s)\n", realBody, content) + return false + } + } + + return true +} diff --git a/src/userstore/user_store.go b/src/userstore/user_store.go index 1f725eb..687b075 100644 --- a/src/userstore/user_store.go +++ b/src/userstore/user_store.go @@ -26,20 +26,22 @@ const ( ) type User struct { - ID uint64 - Name string - Pwd string - Role string + ID uint64 `json:"id,string"` + Name string `json:"name"` + Pwd string `json:"pwd"` + Role string `json:"role"` } type IUserStore interface { Init(rootName, rootPwd string) error IsInited() bool AddUser(user *User) error + DelUser(id uint64) error GetUser(id uint64) (*User, error) GetUserByName(name string) (*User, error) SetName(id uint64, name string) error SetPwd(id uint64, pwd string) error + ListUsers() ([]*User, error) SetRole(id uint64, role string) error AddRole(role string) error DelRole(role string) error @@ -135,6 +137,27 @@ func (us *KVUserStore) AddUser(user *User) error { return us.store.SetStringIn(RolesNs, userID, user.Role) } +func (us *KVUserStore) DelUser(id uint64) error { + us.mtx.Lock() + defer us.mtx.Unlock() + + userID := fmt.Sprint(id) + name, ok := us.store.GetStringIn(NamesNs, userID) + if !ok { + return fmt.Errorf("userID (%s) exists", userID) + } + + // TODO: add complement operations if part of the actions fails + err1 := us.store.DelStringIn(NamesNs, userID) + err2 := us.store.DelStringIn(IDsNs, name) + err3 := us.store.DelStringIn(PwdsNs, userID) + err4 := us.store.DelStringIn(RolesNs, userID) + if err1 != nil || err2 != nil || err3 != nil || err4 != nil { + return fmt.Errorf("get name(%s) id(%s) pwd(%s) role(%s)", err1, err2, err3, err4) + } + return nil +} + func (us *KVUserStore) GetUser(id uint64) (*User, error) { us.mtx.RLock() defer us.mtx.RUnlock() @@ -258,6 +281,37 @@ func (us *KVUserStore) SetRole(id uint64, role string) error { return us.store.SetStringIn(RolesNs, userID, role) } +func (us *KVUserStore) ListUsers() ([]*User, error) { + us.mtx.RLock() + defer us.mtx.RUnlock() + + idToName, err := us.store.ListStringsIn(NamesNs) + if err != nil { + return nil, err + } + + roles, err := us.store.ListStringsIn(RolesNs) + if err != nil { + return nil, err + } + + users := []*User{} + for id, name := range idToName { + intID, err := strconv.ParseUint(id, 10, 64) + if err != nil { + return nil, err + } + + users = append(users, &User{ + ID: intID, + Name: name, + Role: roles[id], + }) + } + + return users, nil +} + func (us *KVUserStore) AddRole(role string) error { us.mtx.Lock() defer us.mtx.Unlock() diff --git a/src/userstore/user_store_test.go b/src/userstore/user_store_test.go index dea570f..e8b6f37 100644 --- a/src/userstore/user_store_test.go +++ b/src/userstore/user_store_test.go @@ -51,6 +51,20 @@ func TestUserStores(t *testing.T) { t.Fatalf("roles not matched %s %s", role1, user.Role) } + users, err := store.ListUsers() + if err != nil { + t.Fatal(err) + } + if len(users) != 2 { + t.Fatalf("users size should be 2 (%d)", len(users)) + } + if users[0].ID != 0 || users[0].Name != rootName || users[0].Role != AdminRole { + t.Fatalf("incorrect root info %v", users[0]) + } + if users[1].ID != 1 || users[1].Name != name1 || users[1].Role != role1 { + t.Fatalf("incorrect user info %v", users[1]) + } + err = store.SetName(id, name2) if err != nil { t.Fatal(err) @@ -91,6 +105,22 @@ func TestUserStores(t *testing.T) { if user.Role != role2 { t.Fatalf("roles not matched %s %s", role2, user.Role) } + + err = store.DelUser(id) + if err != nil { + t.Fatal(err) + } + users, err = store.ListUsers() + if err != nil { + t.Fatal(err) + } + if len(users) != 1 { + t.Fatalf("users size should be 2 (%d)", len(users)) + } + if users[0].ID != 0 || users[0].Name != rootName || users[0].Role != AdminRole { + t.Fatalf("incorrect root info %v", users[0]) + } + } testRoleMethods := func(t *testing.T, store IUserStore) {