Group Creation

This commit is contained in:
Roman Hergenreder 2020-06-24 01:09:08 +02:00
parent d8846ff132
commit d80de63765
7 changed files with 204 additions and 8 deletions

@ -2,8 +2,21 @@
namespace Api { namespace Api {
use Driver\SQL\Condition\Compare;
class GroupsAPI extends Request { class GroupsAPI extends Request {
protected function groupExists($name) {
$sql = $this->user->getSQL();
$res = $sql->select($sql->count())
->from("Group")
->where(new Compare("name", $name))
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
return $this->success && $res[0]["count"] > 0;
}
} }
} }
@ -12,6 +25,7 @@ namespace Api\Groups {
use Api\GroupsAPI; use Api\GroupsAPI;
use Api\Parameter\Parameter; use Api\Parameter\Parameter;
use Api\Parameter\StringType;
class Fetch extends GroupsAPI { class Fetch extends GroupsAPI {
@ -96,4 +110,53 @@ namespace Api\Groups {
} }
} }
class Create extends GroupsAPI {
public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array(
'name' => new StringType('name', 32),
'color' => new StringType('color', 10),
));
$this->loginRequired = true;
$this->requiredGroup = array(USER_GROUP_ADMIN);
}
public function execute($values = array()) {
if (!parent::execute($values)) {
return false;
}
$name = $this->getParam("name");
if (preg_match("/^[a-zA-Z][a-zA-Z0-9_-]*$/", $name) !== 1) {
return $this->createError("Invalid name");
}
$color = $this->getParam("color");
if (preg_match("/^#[a-fA-F0-9]{3,6}$/", $color) !== 1) {
return $this->createError("Invalid color");
}
$exists = $this->groupExists($name);
if (!$this->success) {
return false;
} else if ($exists) {
return $this->createError("A group with this name already exists");
}
$sql = $this->user->getSQL();
$res = $sql->insert("Group", array("name", "color"))
->addRow($name, $color)
->returning("uid")
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
if ($this->success) {
$this->result["uid"] = $sql->getLastInsertId();
}
return $this->success;
}
}
} }

@ -118,7 +118,7 @@ namespace Api\User {
public function __construct($user, $externalCall = false) { public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($user, $externalCall, array(
'username' => new StringType('username', 32), 'username' => new StringType('username', 32),
'email' => new StringType('email', 64, true), 'email' => new Parameter('email', Parameter::TYPE_EMAIL, true, NULL),
'password' => new StringType('password'), 'password' => new StringType('password'),
'confirmPassword' => new StringType('confirmPassword'), 'confirmPassword' => new StringType('confirmPassword'),
)); ));
@ -501,7 +501,7 @@ If the invitation was not intended, you can simply ignore this email.<br><br><a
public function __construct($user, $externalCall = false) { public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($user, $externalCall, array(
"username" => new StringType("username", 32), "username" => new StringType("username", 32),
"email" => new StringType("email", 64), 'email' => new Parameter('email', Parameter::TYPE_EMAIL),
"password" => new StringType("password"), "password" => new StringType("password"),
"confirmPassword" => new StringType("confirmPassword"), "confirmPassword" => new StringType("confirmPassword"),
)); ));
@ -608,7 +608,7 @@ If the registration was not intended, you can simply ignore this email.<br><br><
parent::__construct($user, $externalCall, array( parent::__construct($user, $externalCall, array(
'id' => new Parameter('id', Parameter::TYPE_INT), 'id' => new Parameter('id', Parameter::TYPE_INT),
'username' => new StringType('username', 32, true, NULL), 'username' => new StringType('username', 32, true, NULL),
'email' => new StringType('email', 64, true, NULL), 'email' => new Parameter('email', Parameter::TYPE_EMAIL, true, NULL),
'password' => new StringType('password', -1, true, NULL), 'password' => new StringType('password', -1, true, NULL),
'groups' => new Parameter('groups', Parameter::TYPE_ARRAY, true, NULL), 'groups' => new Parameter('groups', Parameter::TYPE_ARRAY, true, NULL),
)); ));

18
js/admin.min.js vendored

File diff suppressed because one or more lines are too long

@ -80,6 +80,10 @@ export default class API {
} }
async saveRoutes(routes) { async saveRoutes(routes) {
return this.apiCall("routes/save", { "routes": routes }); return this.apiCall("routes/save", { routes: routes });
}
async createGroup(name, color) {
return this.apiCall("groups/create", { name: name, color: color });
} }
}; };

@ -17,6 +17,7 @@ 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"; import EditUser from "./views/edituser";
import CreateGroup from "./views/addgroup";
class AdminDashboard extends React.Component { class AdminDashboard extends React.Component {
@ -89,6 +90,7 @@ class AdminDashboard extends React.Component {
let newProps = {...props, ...this.controlObj}; let newProps = {...props, ...this.controlObj};
return <EditUser {...newProps} /> return <EditUser {...newProps} />
}}/> }}/>
<Route path={"/admin/group/add"}><CreateGroup {...this.controlObj} /></Route>
<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/addgroup.js Normal file

@ -0,0 +1,115 @@
import Alert from "../elements/alert";
import {Link} from "react-router-dom";
import * as React from "react";
import Icon from "../elements/icon";
import ReactTooltip from "react-tooltip";
export default class CreateGroup extends React.Component {
constructor(props) {
super(props);
this.state = {
alerts: [],
isSubmitting: false,
name: "",
color: "#123456"
};
this.parent = {
api: props.api,
};
}
removeAlert(i) {
if (i >= 0 && i < this.state.alerts.length) {
let alerts = this.state.alerts.slice();
alerts.splice(i, 1);
this.setState({...this.state, alerts: alerts});
}
}
render() {
let alerts = [];
for (let i = 0; i < this.state.alerts.length; i++) {
alerts.push(<Alert key={"error-" + i} onClose={() => this.removeAlert(i)} {...this.state.alerts[i]}/>)
}
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">Create a new 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"}>
{alerts}
<form role={"form"} onSubmit={(e) => this.submitForm(e)}>
<div className={"form-group"}>
<label htmlFor={"name"}>Group Name</label>
<input type={"text"} className={"form-control"} placeholder={"Name"}
name={"name"} id={"name"} maxLength={32} value={this.state.name}
onChange={this.onChangeInput.bind(this)}/>
</div>
{/* TODO: add color picker */}
<div className={"form-group"}>
<label htmlFor={"color"}>Color</label>
<input type={"text"} className={"form-control"} placeholder={"Color"}
id={"color"} name={"color"} maxLength={64} value={this.state.color}
onChange={this.onChangeInput.bind(this)}/>
</div>
<Link to={"/admin/users"} className={"btn btn-info mt-2 mr-2"}>
<Icon icon={"arrow-left"}/>
&nbsp;Back
</Link>
{ this.state.isSubmitting
? <button type={"submit"} className={"btn btn-primary mt-2"} disabled>Loading&nbsp;<Icon icon={"circle-notch"} /></button>
: <button type={"submit"} className={"btn btn-primary mt-2"}>Submit</button>
}
</form>
</div>
</div>
</div>
<ReactTooltip/>
</>;
}
onChangeInput(event) {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({ ...this.state, [name]: value });
}
submitForm(e) {
e.preventDefault();
const name = this.state.name;
const color = this.state.color;
this.setState({ ...this.state, isSubmitting: true });
this.parent.api.createGroup(name, color).then((res) => {
let alerts = this.state.alerts.slice();
if (res.success) {
alerts.push({message: "Group was successfully created", title: "Success!", type: "success"});
this.setState({ ...this.state, name: "", color: "", alerts: alerts, isSubmitting: false });
} else {
alerts.push({message: res.msg, title: "Error creating Group", type: "danger"});
this.setState({ ...this.state, name: "", color: "", alerts: alerts, isSubmitting: false });
}
});
}
}

@ -327,7 +327,7 @@ export default class UserOverview extends React.Component {
<div className={"card-header border-0"}> <div className={"card-header border-0"}>
<h3 className={"card-title"}>Groups</h3> <h3 className={"card-title"}>Groups</h3>
<div className={"card-tools"}> <div className={"card-tools"}>
<Link href={"#"} className={"btn btn-tool btn-sm"} to={"/admin/users/addgroup"} > <Link href={"#"} className={"btn btn-tool btn-sm"} to={"/admin/group/add"} >
<Icon icon={"plus"}/> <Icon icon={"plus"}/>
</Link> </Link>
<a href={"#"} className={"btn btn-tool btn-sm"} onClick={() => this.fetchGroups()}> <a href={"#"} className={"btn btn-tool btn-sm"} onClick={() => this.fetchGroups()}>