User Stuff

This commit is contained in:
Roman Hergenreder 2020-06-23 16:26:04 +02:00
parent a4504d8336
commit 7ac6b4d245
7 changed files with 223 additions and 12 deletions

@ -126,7 +126,7 @@ namespace Api\User {
parent::__construct($user, $externalCall, array( parent::__construct($user, $externalCall, array(
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1), 'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
'count' => new Parameter('count', Parameter::TYPE_INT, true, 20), 'count' => new Parameter('count', Parameter::TYPE_INT, true, 20)
)); ));
$this->loginRequired = true; $this->loginRequired = true;
@ -214,6 +214,64 @@ namespace Api\User {
} }
} }
class Get extends UserAPI {
public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array(
'id' => new Parameter('id', Parameter::TYPE_INT)
));
$this->loginRequired = true;
$this->requiredGroup = array(USER_GROUP_SUPPORT, USER_GROUP_ADMIN);
}
public function execute($values = array()) {
if (!parent::execute($values)) {
return false;
}
$id = $this->getParam("id");
$sql = $this->user->getSQL();
$res = $sql->select("User.uid as userId", "User.name", "User.email", "User.registered_at",
"Group.uid as groupId", "Group.name as groupName", "Group.color as groupColor")
->from("User")
->leftJoin("UserGroup", "User.uid", "UserGroup.user_id")
->leftJoin("Group", "Group.uid", "UserGroup.group_id")
->where(new Compare("User.uid", $id))
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
if ($this->success) {
if (empty($res)) {
return $this->createError("User not found");
} else {
$row = $res[0];
$this->result["user"] = array(
"uid" => $row["userId"],
"name" => $row["name"],
"email" => $row["email"],
"registered_at" => $row["registered_at"],
"groups" => array()
);
foreach($res as $row) {
$this->result["user"]["groups"][] = array(
"uid" => $row["groupId"],
"name" => $row["groupName"],
"color" => $row["groupColor"],
);
}
}
}
return $this->success;
}
}
class Info extends UserAPI { class Info extends UserAPI {
public function __construct($user, $externalCall = false) { public function __construct($user, $externalCall = false) {

20
js/admin.min.js vendored

File diff suppressed because one or more lines are too long

@ -43,6 +43,10 @@ export default class API {
return this.apiCall("notifications/fetch"); return this.apiCall("notifications/fetch");
} }
async getUser(id) {
return this.apiCall("user/get", { id: id });
}
async fetchUsers(pageNum = 1, count = 20) { async fetchUsers(pageNum = 1, count = 20) {
return this.apiCall("user/fetch", { page: pageNum, count: count }); return this.apiCall("user/fetch", { page: pageNum, count: count });
} }

@ -4,7 +4,7 @@ import React from "react";
export default function Alert(props) { export default function Alert(props) {
const onClose = props.onClose || function() { }; const onClose = props.onClose || null;
const title = props.title || "Untitled Alert"; const title = props.title || "Untitled Alert";
const message = props.message || "Alert message"; const message = props.message || "Alert message";
const type = props.type || "danger"; const type = props.type || "danger";
@ -18,7 +18,7 @@ export default function Alert(props) {
return ( return (
<div className={"alert alert-" + type + " alert-dismissible"}> <div className={"alert alert-" + type + " alert-dismissible"}>
<button type="button" className={"close"} data-dismiss={"alert"} aria-hidden={"true"} onClick={onClose}>×</button> {onClose ? <button type="button" className={"close"} data-dismiss={"alert"} aria-hidden={"true"} onClick={onClose}>×</button> : null}
<h5><Icon icon={icon} className={"icon"} /> {title}</h5> <h5><Icon icon={icon} className={"icon"} /> {title}</h5>
{message} {message}
</div> </div>

@ -16,6 +16,7 @@ import Logs from "./views/logs";
import PageOverview from "./views/pages"; import PageOverview from "./views/pages";
import HelpPage from "./views/help"; import HelpPage from "./views/help";
import Footer from "./footer"; import Footer from "./footer";
import EditUser from "./views/edituser";
class AdminDashboard extends React.Component { class AdminDashboard extends React.Component {
@ -83,7 +84,11 @@ class AdminDashboard extends React.Component {
<Switch> <Switch>
<Route path={"/admin/dashboard"}><Overview {...this.controlObj} notifications={this.state.notifications} /></Route> <Route path={"/admin/dashboard"}><Overview {...this.controlObj} notifications={this.state.notifications} /></Route>
<Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route> <Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route>
<Route exact={true} path={"/admin/users/adduser"}><CreateUser {...this.controlObj} /></Route> <Route path={"/admin/user/add"}><CreateUser {...this.controlObj} /></Route>
<Route path={"/admin/user/edit/:userId"} render={(props) => {
let newProps = {...props, ...this.controlObj};
return <EditUser {...newProps} />
}}/>
<Route path={"/admin/logs"}><Logs {...this.controlObj} /></Route> <Route path={"/admin/logs"}><Logs {...this.controlObj} /></Route>
<Route path={"/admin/pages"}><PageOverview {...this.controlObj} /></Route> <Route path={"/admin/pages"}><PageOverview {...this.controlObj} /></Route>
<Route path={"/admin/help"}><HelpPage {...this.controlObj} /></Route> <Route path={"/admin/help"}><HelpPage {...this.controlObj} /></Route>

115
src/src/views/edituser.js Normal file

@ -0,0 +1,115 @@
import * as React from "react";
import Icon from "../elements/icon";
import Alert from "../elements/alert";
import {Link} from "react-router-dom";
export default class EditUser extends React.Component {
constructor(props) {
super(props);
this.parent = {
api: props.api
};
this.state = {
user: {},
errors: [],
fetchError: null,
loaded: false,
isSaving: false
}
}
removeError(i) {
if (i >= 0 && i < this.state.errors.length) {
let errors = this.state.errors.slice();
errors.splice(i, 1);
this.setState({...this.state, errors: errors});
}
}
componentDidMount() {
this.parent.api.getUser(this.props.match.params["userId"]).then((res) => {
if (res.success) {
this.setState({ ...this.state, user: res.user, loaded: true });
} else {
this.setState({ ...this.state, fetchError: res.msg, loaded: true });
}
});
}
render() {
if (!this.state.loaded) {
return <h2 className={"text-center"}>
Loading<br/>
<Icon icon={"spinner"} className={"mt-3 text-muted fa-2x"}/>
</h2>
}
let errors = [];
let form = null;
if(this.state.fetchError) {
errors.push(
<Alert key={"error-fetch"} title={"Error fetching user details"} type={"danger"} message={
<div>{this.state.fetchError}<br/>You can meanwhile return to the&nbsp;
<Link to={"/admin/users"}>user overview</Link>
</div>
}/>
)
} else {
for (let i = 0; i < this.state.errors.length; i++) {
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.errors[i]}/>)
}
form = <form role={"form"} onSubmit={(e) => e.preventDefault()}>
<div className={"form-group"}>
<label htmlFor={"username"}>Username</label>
<input type={"text"} className={"form-control"} placeholder={"Enter username"}
name={"username"} id={"username"} maxLength={32} value={this.state.user.name}/>
</div>
<div className={"form-group"}>
<label htmlFor={"email"}>E-Mail</label>
<input type={"email"} className={"form-control"} placeholder={"E-Mail address"}
id={"email"} name={"email"} maxLength={64} value={this.state.user.email} />
</div>
<Link to={"/admin/users"} className={"btn btn-info mt-2 mr-2"}>
<Icon icon={"arrow-left"}/>
&nbsp;Back
</Link>
{ this.state.isSaving
? <button type={"submit"} className={"btn btn-primary mt-2"} disabled>Saving&nbsp;<Icon icon={"circle-notch"} /></button>
: <button type={"submit"} className={"btn btn-primary mt-2"}>Save</button>
}
</form>
}
return <>
<div className="content-header">
<div className="container-fluid">
<div className="row mb-2">
<div className="col-sm-6">
<h1 className="m-0 text-dark">Edit User</h1>
</div>
<div className="col-sm-6">
<ol className="breadcrumb float-sm-right">
<li className="breadcrumb-item"><Link to={"/admin/dashboard"}>Home</Link></li>
<li className="breadcrumb-item"><Link to={"/admin/users"}>Users</Link></li>
<li className="breadcrumb-item active">Add User</li>
</ol>
</div>
</div>
</div>
</div>
<div className={"content"}>
<div className={"row"}>
<div className={"col-lg-6 pl-5 pr-5"}>
{errors}
{form}
</div>
</div>
</div>
</>
}
}

@ -3,6 +3,7 @@ import Icon from "../elements/icon";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {getPeriodString} from "../global"; import {getPeriodString} from "../global";
import Alert from "../elements/alert"; import Alert from "../elements/alert";
import ReactTooltip from "react-tooltip";
const TABLE_SIZE = 10; const TABLE_SIZE = 10;
@ -77,7 +78,7 @@ export default class UserOverview extends React.Component {
totalCount: res.totalCount, totalCount: res.totalCount,
} }
}); });
this.rowCount = Math.max(this.rowCount, Object.keys(res.groups).length); this.rowCount = Math.max(this.rowCount, Object.keys(res.users).length);
} else { } else {
let errors = this.state.errors.slice(); let errors = this.state.errors.slice();
errors.push({title: "Error fetching users", message: res.msg}); errors.push({title: "Error fetching users", message: res.msg});
@ -145,6 +146,7 @@ export default class UserOverview extends React.Component {
</div> </div>
</div> </div>
</div> </div>
<ReactTooltip />
</>; </>;
} }
@ -176,7 +178,20 @@ export default class UserOverview extends React.Component {
<td>{user.name}</td> <td>{user.name}</td>
<td>{user.email}</td> <td>{user.email}</td>
<td>{groups}</td> <td>{groups}</td>
<td>{getPeriodString(user.registered_at)}</td> <td>
<span data-effect={"solid"}
data-tip={user.registered_at}
data-place={"bottom"}>
{getPeriodString(user.registered_at)}
</span>
</td>
<td>
<Link to={"/admin/user/edit/" + uid} className={"text-reset"}>
<Icon icon={"pencil-alt"} data-effect={"solid"}
data-tip={"Modify user details & group membership"}
data-type={"info"} data-place={"right"}/>
</Link>
</td>
</tr> </tr>
); );
} }
@ -188,6 +203,7 @@ export default class UserOverview extends React.Component {
<td/> <td/>
<td/> <td/>
<td/> <td/>
<td/>
</tr> </tr>
); );
} }
@ -211,7 +227,7 @@ export default class UserOverview extends React.Component {
<div className={"card-header border-0"}> <div className={"card-header border-0"}>
<h3 className={"card-title"}>Users</h3> <h3 className={"card-title"}>Users</h3>
<div className={"card-tools"}> <div className={"card-tools"}>
<Link href={"#"} className={"btn btn-tool btn-sm"} to={"/admin/users/adduser"} > <Link href={"#"} className={"btn btn-tool btn-sm"} to={"/admin/user/add"} >
<Icon icon={"plus"}/> <Icon icon={"plus"}/>
</Link> </Link>
<a href={"#"} className={"btn btn-tool btn-sm"} onClick={() => this.fetchUsers()}> <a href={"#"} className={"btn btn-tool btn-sm"} onClick={() => this.fetchUsers()}>
@ -227,6 +243,7 @@ export default class UserOverview extends React.Component {
<th>Email</th> <th>Email</th>
<th>Groups</th> <th>Groups</th>
<th>Registered</th> <th>Registered</th>
<th/>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -273,7 +290,7 @@ export default class UserOverview extends React.Component {
<td>{group.name}</td> <td>{group.name}</td>
<td className={"text-center"}>{group.memberCount}</td> <td className={"text-center"}>{group.memberCount}</td>
<td> <td>
<span className={"badge text-white mr-1"} style={{backgroundColor: group.color}}> <span className={"badge text-white mr-1"} style={{backgroundColor: group.color, fontFamily: "monospace"}}>
{group.color} {group.color}
</span> </span>
</td> </td>