Create User + Chart dependency

This commit is contained in:
Roman Hergenreder 2020-06-17 20:20:31 +02:00
parent c369a6aec1
commit 373808879a
11 changed files with 296 additions and 79 deletions

10
admin/dist/main.js vendored

File diff suppressed because one or more lines are too long

@ -1349,6 +1349,11 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
}, },
"@reach/observe-rect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.1.0.tgz",
"integrity": "sha512-kE+jvoj/OyJV24C03VvLt5zclb9ArJi04wWXMMFwQvdZjdHoBlN4g0ZQFjyy/ejPF1Z/dpUD5dhRdBiUmIGZTA=="
},
"@sheerun/mutationobserver-shim": { "@sheerun/mutationobserver-shim": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz",
@ -4240,6 +4245,80 @@
"type": "^1.0.1" "type": "^1.0.1"
} }
}, },
"d3-array": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz",
"integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw=="
},
"d3-color": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
"integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
},
"d3-delaunay": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.3.0.tgz",
"integrity": "sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==",
"requires": {
"delaunator": "4"
}
},
"d3-format": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.4.tgz",
"integrity": "sha512-TWks25e7t8/cqctxCmxpUuzZN11QxIA7YrMbram94zMQ0PXjE4LVIMe/f6a4+xxL8HQ3OsAFULOINQi1pE62Aw=="
},
"d3-interpolate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz",
"integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==",
"requires": {
"d3-color": "1"
}
},
"d3-path": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
},
"d3-scale": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.1.tgz",
"integrity": "sha512-huz5byJO/6MPpz6Q8d4lg7GgSpTjIZW/l+1MQkzKfu2u8P6hjaXaStOpmyrD6ymKoW87d2QVFCKvSjLwjzx/rA==",
"requires": {
"d3-array": "1.2.0 - 2",
"d3-format": "1",
"d3-interpolate": "^1.2.0",
"d3-time": "1",
"d3-time-format": "2"
}
},
"d3-shape": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
"requires": {
"d3-path": "1"
}
},
"d3-time": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
"integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="
},
"d3-time-format": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.2.3.tgz",
"integrity": "sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==",
"requires": {
"d3-time": "1"
}
},
"d3-voronoi": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz",
"integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg=="
},
"damerau-levenshtein": { "damerau-levenshtein": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
@ -4415,6 +4494,11 @@
} }
} }
}, },
"delaunator": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz",
"integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag=="
},
"delayed-stream": { "delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -10442,6 +10526,18 @@
"whatwg-fetch": "^3.0.0" "whatwg-fetch": "^3.0.0"
} }
}, },
"react-charts": {
"version": "2.0.0-beta.7",
"resolved": "https://registry.npmjs.org/react-charts/-/react-charts-2.0.0-beta.7.tgz",
"integrity": "sha512-iUspg9rnx7kD0H/wsK67HNUioOgKgJ8WRXr/Tk3EGP2qcFb9Vo7pjDk4oz1jH12TC+mqL+HFxNYraMkhWd6CUw==",
"requires": {
"@reach/observe-rect": "^1.1.0",
"d3-delaunay": "^5.2.1",
"d3-scale": "^3.2.1",
"d3-shape": "^1.3.7",
"d3-voronoi": "^1.1.2"
}
},
"react-dev-utils": { "react-dev-utils": {
"version": "10.2.1", "version": "10.2.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz",

@ -7,13 +7,13 @@ import {Link} from "react-router-dom";
export default function Header(props) { export default function Header(props) {
const parent = { const parent = {
notifications: props.notifications || { }, notifications: props.notifications || [ ],
}; };
const [dropdownVisible, showDropdown] = useState(false); const [dropdownVisible, showDropdown] = useState(false);
const mailIcon = <Icon icon={"envelope"} type={"fas"} />; const mailIcon = <Icon icon={"envelope"} type={"fas"} />;
let notificationCount = Object.keys(parent.notifications).length; let notificationCount = parent.notifications.length;
let notificationText = "No new notifications"; let notificationText = "No new notifications";
if(notificationCount === 1) { if(notificationCount === 1) {
@ -23,8 +23,9 @@ export default function Header(props) {
} }
let notificationItems = []; let notificationItems = [];
for (let uid in parent.notifications) { for (let i = 0; i < parent.notifications.length; i++) {
const notification = parent.notifications[uid]; const notification = parent.notifications[i];
const uid = notification.uid;
const createdAt = getPeriodString(notification.created_at); const createdAt = getPeriodString(notification.created_at);
notificationItems.push( notificationItems.push(
<Link to={"/admin/logs?notification=" + uid} className={"dropdown-item"} key={"notification-" + uid}> <Link to={"/admin/logs?notification=" + uid} className={"dropdown-item"} key={"notification-" + uid}>

@ -24,10 +24,6 @@ class AdminDashboard extends React.Component {
dialog: { onClose: () => this.hideDialog() }, dialog: { onClose: () => this.hideDialog() },
notifications: { } notifications: { }
}; };
this.controlObj = {
showDialog: this.showDialog.bind(this),
api: this.api
};
} }
onUpdate() { onUpdate() {
@ -71,16 +67,21 @@ class AdminDashboard extends React.Component {
return <b>Loading <Icon icon={"spinner"} /></b> return <b>Loading <Icon icon={"spinner"} /></b>
} }
this.controlObj = {
showDialog: this.showDialog.bind(this),
api: this.api
};
return <Router> return <Router>
<Header {...this.controlObj} notifications={this.state.notifications} /> <Header {...this.controlObj} notifications={this.state.notifications} />
<Sidebar {...this.controlObj} notifications={this.state.notifications} /> <Sidebar {...this.controlObj} notifications={this.state.notifications} />
<div className={"content-wrapper p-2"}> <div className={"content-wrapper p-2"}>
<section className={"content"}> <section className={"content"}>
<Switch> <Switch>
<Route path={"/admin/dashboard"}><Overview {...this.controlObj} /></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 exact={true} path={"/admin/users/adduser"}><CreateUser {...this.controlObj} /></Route>
<Route path={"/admin/logs"}><Logs {...this.controlObj} /></Route> <Route path={"/admin/logs"}><Logs {...this.controlObj} notifications={this.state.notifications} /></Route>
<Route path={"*"}><View404 /></Route> <Route path={"*"}><View404 /></Route>
</Switch> </Switch>
<Dialog {...this.state.dialog}/> <Dialog {...this.state.dialog}/>

@ -6,7 +6,8 @@ export default function Sidebar(props) {
let parent = { let parent = {
showDialog: props.showDialog || function() {}, showDialog: props.showDialog || function() {},
api: props.api api: props.api,
notifications: props.notifications || [ ]
}; };
function onLogout() { function onLogout() {
@ -28,6 +29,10 @@ export default function Sidebar(props) {
"name": "Users & Groups", "name": "Users & Groups",
"icon": "users" "icon": "users"
}, },
"pages": {
"name": "Pages & Routes",
"icon": "copy",
},
"settings": { "settings": {
"name": "Settings", "name": "Settings",
"icon": "tools" "icon": "tools"
@ -42,7 +47,7 @@ export default function Sidebar(props) {
}, },
}; };
let numNotifications = Object.keys(props.notifications).length; let numNotifications = parent.notifications.length;
if (numNotifications > 0) { if (numNotifications > 0) {
if (numNotifications > 9) numNotifications = "9+"; if (numNotifications > 9) numNotifications = "9+";
menuItems["logs"]["badge"] = { type: "warning", value: numNotifications }; menuItems["logs"]["badge"] = { type: "warning", value: numNotifications };
@ -88,15 +93,11 @@ export default function Sidebar(props) {
<div className={"os-padding"}> <div className={"os-padding"}>
<div className={"os-viewport os-viewport-native-scrollbars-invisible"} style={{right: "0px", bottom: "0px"}}> <div className={"os-viewport os-viewport-native-scrollbars-invisible"} style={{right: "0px", bottom: "0px"}}>
<div className={"os-content"} style={{padding: "0px 0px", height: "100%", width: "100%"}}> <div className={"os-content"} style={{padding: "0px 0px", height: "100%", width: "100%"}}>
{/* LOGGED IN AS */}
<div className="user-panel mt-3 pb-3 mb-3 d-flex"> <div className="user-panel mt-3 pb-3 mb-3 d-flex">
<div className="info"> <div className="info">
<a href="#" className="d-block">Logged in as: {parent.api.user.name}</a> <a href="#" className="d-block">Logged in as: {parent.api.user.name}</a>
</div> </div>
</div> </div>
{/* SIDEBAR */}
<nav className={"mt-2"}> <nav className={"mt-2"}>
<ul className={"nav nav-pills nav-sidebar flex-column"} data-widget={"treeview"} role={"menu"} data-accordion={"false"}> <ul className={"nav nav-pills nav-sidebar flex-column"} data-widget={"treeview"} role={"menu"} data-accordion={"false"}>
{li} {li}

@ -146,7 +146,7 @@ export default class CreateUser extends React.Component {
this.parent.api.inviteUser(username, email).then((res) => { this.parent.api.inviteUser(username, email).then((res) => {
let errors = this.state.errors.slice(); let errors = this.state.errors.slice();
if (!res.success) { if (!res.success) {
errors.push({ title: "Error inviting User", message: res.msg, type: "error" }); errors.push({ title: "Error inviting User", message: res.msg, type: "danger" });
this.setState({ ...this.state, errors: errors }); this.setState({ ...this.state, errors: errors });
} else { } else {
errors.push({ title: "Success", message: "The invitation was successfully sent.", type: "success" }); errors.push({ title: "Success", message: "The invitation was successfully sent.", type: "success" });
@ -157,7 +157,7 @@ export default class CreateUser extends React.Component {
this.parent.api.createUser(username, email, password, confirmPassword).then((res) => { this.parent.api.createUser(username, email, password, confirmPassword).then((res) => {
let errors = this.state.errors.slice(); let errors = this.state.errors.slice();
if (!res.success) { if (!res.success) {
errors.push({ title: "Error creating User", message: res.msg, type: "error" }); errors.push({ title: "Error creating User", message: res.msg, type: "danger" });
this.setState({ ...this.state, errors: errors, password: "", confirmPassword: "" }); this.setState({ ...this.state, errors: errors, password: "", confirmPassword: "" });
} else { } else {
errors.push({ title: "Success", message: "The user was successfully created.", type: "success" }); errors.push({ title: "Success", message: "The user was successfully created.", type: "success" });

@ -1,9 +1,130 @@
import * as React from "react"; import * as React from "react";
import {Link} from "react-router-dom";
import Icon from "../elements/icon";
export default class Overview extends React.Component { export default class Overview extends React.Component {
constructor(props) {
super(props);
this.parent = {
showDialog: props.showDialog,
notifications: props.notification,
api: props.api,
}
}
fetchStats() {
}
render() { render() {
return <div>Overview</div>
let userCount = 0;
let notificationCount = 0;
let pageCount = 0;
let visitorCount = 0;
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"}>Dashboard</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 active">Dashboard</li>
</ol>
</div>
</div>
</div>
</div>
<section className={"content"}>
<div className={"container-fluid"}>
<div className={"row"}>
<div className={"col-lg-3 col-6"}>
<div className="small-box bg-info">
<div className={"inner"}>
<h3>{userCount}</h3>
<p>Users registered</p>
</div>
<div className="icon">
<Icon icon={"users"} />
</div>
<Link to={"/admin/users"} className="small-box-footer">More info <Icon icon={"arrow-circle-right"}/></Link>
</div>
</div>
<div className={"col-lg-3 col-6"}>
<div className={"small-box bg-success"}>
<div className={"inner"}>
<h3>{pageCount}</h3>
<p>Routes & Pages</p>
</div>
<div className="icon">
<Icon icon={"copy"} />
</div>
<Link to={"/admin/pages"} className="small-box-footer">More info <Icon icon={"arrow-circle-right"}/></Link>
</div>
</div>
<div className={"col-lg-3 col-6"}>
<div className={"small-box bg-warning"}>
<div className={"inner"}>
<h3>{notificationCount}</h3>
<p>new Notifications</p>
</div>
<div className={"icon"}>
<Icon icon={"bell"} />
</div>
<Link to={"/admin/logs"} className="small-box-footer">More info <Icon icon={"arrow-circle-right"}/></Link>
</div>
</div>
<div className={"col-lg-3 col-6"}>
<div className={"small-box bg-danger"}>
<div className={"inner"}>
<h3>{visitorCount}</h3>
<p>Unique Visitors</p>
</div>
<div className="icon">
<Icon icon={"chart-line"} />
</div>
<Link to={"/admin/statistics"} className="small-box-footer">More info <Icon icon={"arrow-circle-right"}/></Link>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-lg-6 col-12">
<div className="card card-info">
<div className="card-header">
<h3 className="card-title">Unique Visitors this year</h3>
<div className="card-tools">
<button type="button" className="btn btn-tool" data-card-widget="collapse">
<Icon icon={"minus"} />
</button>
<button type="button" className="btn btn-tool" data-card-widget="remove">
<Icon icon={"times"} />
</button>
</div>
</div>
<div className="card-body">
<div className="chart">
<div className="chartjs-size-monitor">
<div className="chartjs-size-monitor-expand">
<div/>
</div>
<div className="chartjs-size-monitor-shrink">
<div/>
</div>
</div>
<BarChart data={data} series={series} axes={axes} tooltip />
</div>
</div>
</div>
</div>
</div>
</section>
</>
} }
} }

@ -65,7 +65,7 @@ class Fetch extends Request {
foreach($res as $row) { foreach($res as $row) {
$id = $row["uid"]; $id = $row["uid"];
if (!isset($this->notifications[$id])) { if (!isset($this->notifications[$id])) {
$this->notifications[$id] = array( $this->notifications[] = array(
"uid" => $id, "uid" => $id,
"title" => $row["title"], "title" => $row["title"],
"message" => $row["message"], "message" => $row["message"],

@ -21,70 +21,61 @@ class Create extends Request {
} }
public function execute($values = array()) { public function execute($values = array()) {
if(!parent::execute($values)) { if (!parent::execute($values)) {
return false; return false;
} }
$username = $this->getParam('username'); $username = $this->getParam('username');
$email = $this->getParam('email'); $email = $this->getParam('email');
if (!$this->userExists($username, $email) || !$this->success) {
if(!$this->userExists($username, $email) || !$this->success) { return false;
return false;
} }
$password = $this->getParam('password'); $password = $this->getParam('password');
$confirmPassword = $this->getParam('confirmPassword'); $confirmPassword = $this->getParam('confirmPassword');
if ($password !== $confirmPassword) {
if($password !== $confirmPassword) { return false;
return false;
} }
$sql = $this->user->getSQL();
$this->success = $this->createUser($username, $email, $password); $this->success = $this->createUser($username, $email, $password);
return $this->success;
}
private function userExists($username, $email) {
$sql = $this->user->getSQL();
$res = $sql->select("User.name", "User.email")
->from("User")
->where(new Compare("User.name", $username), new Compare("User.email", $email))
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
if (!empty($res)) {
$row = $res[0];
if (strcasecmp($username, $row['name']) === 0) {
$this->lastError = "This username is already in use.";
$this->success = false;
} else if (strcasecmp($username, $row['email']) === 0) {
$this->lastError = "This email address is already taken";
$this->success = false;
}
}
return $this->success; return $this->success;
} }
private function userExists($username, $email){ private function createUser($username, $email, $password) {
$sql = $this->user->getSQL(); $sql = $this->user->getSQL();
$res = $sql->select("User.name", "User.email") $salt = generateRandomString(16);
->from("User") $hash = hash('sha256', $password . $salt);
->where(new Compare("User.name", $username), new Compare("User.email",$email)) $res = $sql->insert("User", array(
->execute(); 'username' => $username,
'password' => $hash,
$this->success = ($res !== FALSE); 'salt' => $salt,
$this->lastError = $sql->getLastError(); 'email' => $email
))->execute();
if($res !== 0) { $this->lastError = $sql->getLastError();
$this->success = false; return $res === TRUE;
$row = $res[0];
$message = "";
if (strcmp($username,row['name']) != 0 && strcmp($email, row['email']) != 0) {
$message = "Username and email are already taken";
}else if (strcmp($username,row['name']) != 0) {
$message = "Username is already taken";
}else{
$message = "Email is already taken";
}
$this->lastError = $message;
return true;
}
return false;
}
private function createUser($username, $email, $password){
$sql = $this->user->getSQL();
$salt = generateRandomString(16);
$hash = hash('sha256', $password . $salt);
$res = $sql->insert("User",array(
'username' => $username,
'password' => $hash,
'email' => $email
))->execute();
$this->lastError = $sql->getLastError();
return $res === TRUE;
} }
} }

@ -51,6 +51,12 @@ class CreateDatabase {
->primaryKey("uid", "user_id") ->primaryKey("uid", "user_id")
->foreignKey("user_id", "User", "uid", new CascadeStrategy()); ->foreignKey("user_id", "User", "uid", new CascadeStrategy());
$queries[] = $sql->createTable("UserInvitation")
->addString("username",32)
->addString("email",32)
->addString("token",36)
->addDateTime("valid_until");
$queries[] = $sql->createTable("UserToken") $queries[] = $sql->createTable("UserToken")
->addInt("user_id") ->addInt("user_id")
->addString("token", 36) ->addString("token", 36)

10
js/admin.min.js vendored

File diff suppressed because one or more lines are too long