Frontend, Bugfixes

This commit is contained in:
Roman Hergenreder 2020-04-04 01:15:59 +02:00
parent efe3ada470
commit 8ce74edc38
30 changed files with 501 additions and 132 deletions

@ -2,14 +2,38 @@
namespace Api\User; namespace Api\User;
use Api\Parameter\Parameter;
use \Api\Request; use \Api\Request;
class Fetch extends Request { class Fetch extends Request {
const SELECT_SIZE = 20;
private int $userCount;
public function __construct($user, $externalCall = false) { public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array());
parent::__construct($user, $externalCall, array(
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1)
));
$this->loginRequired = true; $this->loginRequired = true;
$this->requiredGroup = USER_GROUP_ADMIN; $this->requiredGroup = USER_GROUP_ADMIN;
$this->userCount = 0;
}
private function getUserCount() {
$sql = $this->user->getSQL();
$res = $sql->select($sql->count())->from("User")->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
if ($this->success) {
$this->userCount = $res[0]["count"];
}
return $this->success;
} }
public function execute($values = array()) { public function execute($values = array()) {
@ -17,11 +41,25 @@ class Fetch extends Request {
return false; return false;
} }
$page = $this->getParam("page");
if($page < 1) {
return $this->createError("Invalid page count");
}
if (!$this->getUserCount()) {
return false;
}
$sql = $this->user->getSQL(); $sql = $this->user->getSQL();
$res = $sql->select("User.uid as userId", "User.name", "User.email", "Group.uid as groupId", "Group.name as groupName") $res = $sql->select("User.uid as userId", "User.name", "User.email", "User.created_at",
"Group.uid as groupId", "Group.name as groupName")
->from("User") ->from("User")
->leftJoin("UserGroup", "User.uid", "UserGroup.user_id") ->leftJoin("UserGroup", "User.uid", "UserGroup.user_id")
->leftJoin("Group", "Group.uid", "UserGroup.group_id") ->leftJoin("Group", "Group.uid", "UserGroup.group_id")
->orderBy("User.uid")
->ascending()
->limit(Fetch::SELECT_SIZE)
->offset(($page - 1) * Fetch::SELECT_SIZE)
->execute(); ->execute();
$this->success = ($res !== FALSE); $this->success = ($res !== FALSE);
@ -30,14 +68,15 @@ class Fetch extends Request {
if($this->success) { if($this->success) {
$this->result["users"] = array(); $this->result["users"] = array();
foreach($res as $row) { foreach($res as $row) {
$userId = $row["userId"]; $userId = intval($row["userId"]);
$groupId = $row["groupId"]; $groupId = intval($row["groupId"]);
$groupName = $row["groupName"]; $groupName = $row["groupName"];
if (!isset($this->result["users"][$userId])) { if (!isset($this->result["users"][$userId])) {
$this->result["users"][$userId] = array( $this->result["users"][$userId] = array(
"uid" => $userId, "uid" => $userId,
"name" => $row["name"], "name" => $row["name"],
"email" => $row["email"], "email" => $row["email"],
"created_at" => $row["created_at"],
"groups" => array(), "groups" => array(),
); );
} }
@ -46,6 +85,7 @@ class Fetch extends Request {
$this->result["users"][$userId]["groups"][$groupId] = $groupName; $this->result["users"][$userId]["groups"][$groupId] = $groupName;
} }
} }
$this->result["pages"] = intval(ceil($this->userCount / Fetch::SELECT_SIZE));
} }
return $this->success; return $this->success;

@ -31,6 +31,7 @@ class CreateDatabase {
->addString("salt", 16) ->addString("salt", 16)
->addString("password", 64) ->addString("password", 64)
->addInt("language_id", true, 1) ->addInt("language_id", true, 1)
->addDateTime("registered_at", false, $sql->currentTimestamp())
->primaryKey("uid") ->primaryKey("uid")
->unique("email") ->unique("email")
->unique("name") ->unique("name")

@ -5,12 +5,12 @@ namespace Documents {
use Documents\Admin\AdminHead; use Documents\Admin\AdminHead;
use Elements\Document; use Elements\Document;
use Objects\User; use Objects\User;
use Views\AdminDashboard; use Views\Admin\AdminDashboardBody;
use Views\LoginBody; use Views\LoginBody;
class Admin extends Document { class Admin extends Document {
public function __construct(User $user) { public function __construct(User $user) {
$body = $user->isLoggedIn() ? AdminDashboard::class : LoginBody::class; $body = $user->isLoggedIn() ? AdminDashboardBody::class : LoginBody::class;
parent::__construct($user, AdminHead::class, $body); parent::__construct($user, AdminHead::class, $body);
} }
} }

@ -17,6 +17,7 @@ namespace Documents\Document404 {
use Elements\Body; use Elements\Body;
use Elements\Head; use Elements\Head;
use Views\View404;
class Head404 extends Head { class Head404 extends Head {
@ -25,11 +26,6 @@ namespace Documents\Document404 {
} }
protected function initSources() { protected function initSources() {
// $this->loadJQuery();
// $this->loadBootstrap();
// $this->loadFontawesome();
// $this->addJS(\Elements\Script::CORE);
// $this->addCSS(\Elements\Link::CORE);
} }
protected function initMetas() { protected function initMetas() {
@ -59,7 +55,7 @@ namespace Documents\Document404 {
public function getCode() { public function getCode() {
$html = parent::getCode(); $html = parent::getCode();
$html .= "<b>404 Not Found</b>"; $html .= "<body>" . (new View404($this->getDocument())) . "</body>";
return $html; return $html;
} }
} }

@ -17,6 +17,7 @@ namespace Documents {
namespace Documents\Install { namespace Documents\Install {
use Api\Notifications\Create; use Api\Notifications\Create;
use Api\Parameter\Parameter;
use Configuration\CreateDatabase; use Configuration\CreateDatabase;
use Driver\SQL\SQL; use Driver\SQL\SQL;
use Elements\Body; use Elements\Body;
@ -289,8 +290,8 @@ namespace Documents\Install {
$username = $this->getParameter("username"); $username = $this->getParameter("username");
$password = $this->getParameter("password"); $password = $this->getParameter("password");
$confirmPassword = $this->getParameter("confirmPassword"); $confirmPassword = $this->getParameter("confirmPassword");
$email = $this->getParameter("email") ?? "";
$msg = $this->errorString;
$success = true; $success = true;
$missingInputs = array(); $missingInputs = array();
@ -321,13 +322,16 @@ namespace Documents\Install {
} else if(strlen($password) < 6) { } else if(strlen($password) < 6) {
$msg = "The password should be at least 6 characters long"; $msg = "The password should be at least 6 characters long";
$success = false; $success = false;
} else if($email && Parameter::parseType($email) !== Parameter::TYPE_EMAIL) {
$msg = "Invalid email address";
$success = false;
} else { } else {
$salt = generateRandomString(16); $salt = generateRandomString(16);
$hash = hash('sha256', $password . $salt); $hash = hash('sha256', $password . $salt);
$sql = $user->getSQL(); $sql = $user->getSQL();
$success = $sql->insert("User", array("name", "salt", "password")) $success = $sql->insert("User", array("name", "salt", "password", "email"))
->addRow($username, $salt, $hash) ->addRow($username, $salt, $hash, $email)
->returning("uid") ->returning("uid")
->execute() ->execute()
&& $sql->insert("UserGroup", array("group_id", "user_id")) && $sql->insert("UserGroup", array("group_id", "user_id"))
@ -606,6 +610,7 @@ namespace Documents\Install {
"title" => "Create a User", "title" => "Create a User",
"form" => array( "form" => array(
array("title" => "Username", "name" => "username", "type" => "text", "required" => true), array("title" => "Username", "name" => "username", "type" => "text", "required" => true),
array("title" => "Email", "name" => "email", "type" => "text"),
array("title" => "Password", "name" => "password", "type" => "password", "required" => true), array("title" => "Password", "name" => "password", "type" => "password", "required" => true),
array("title" => "Confirm Password", "name" => "confirmPassword", "type" => "password", "required" => true), array("title" => "Confirm Password", "name" => "confirmPassword", "type" => "password", "required" => true),
), ),
@ -704,7 +709,7 @@ namespace Documents\Install {
$id = $button["id"]; $id = $button["id"];
$float = $button["float"]; $float = $button["float"];
$disabled = (isset($button["disabled"]) && $button["disabled"]) ? " disabled" : ""; $disabled = (isset($button["disabled"]) && $button["disabled"]) ? " disabled" : "";
$button = "<button type=\"button\" id=\"$id\" class=\"btn btn-$type margin-xs\"$disabled>$title</button>"; $button = "<button type=\"button\" id=\"$id\" class=\"btn btn-$type m-1\"$disabled>$title</button>";
if($float === "left") { if($float === "left") {
$buttonsLeft .= $button; $buttonsLeft .= $button;

@ -7,7 +7,7 @@ class CondOr extends Condition {
private array $conditions; private array $conditions;
public function __construct(...$conditions) { public function __construct(...$conditions) {
$this->conditions = $conditions; $this->conditions = (!empty($conditions) && is_array($conditions[0])) ? $conditions[0] : $conditions;
} }
public function getConditions() { return $this->conditions; } public function getConditions() { return $this->conditions; }

@ -2,6 +2,8 @@
namespace Driver\SQL\Query; namespace Driver\SQL\Query;
use Driver\SQL\Condition\CondOr;
class Delete extends Query { class Delete extends Query {
private string $table; private string $table;
@ -14,7 +16,7 @@ class Delete extends Query {
} }
public function where(...$conditions) { public function where(...$conditions) {
$this->conditions = array_merge($this->conditions, $conditions); $this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions));
return $this; return $this;
} }

@ -7,9 +7,16 @@ use Driver\SQL\SQL;
abstract class Query { abstract class Query {
protected SQL $sql; protected SQL $sql;
public bool $dump;
public function __construct($sql) { public function __construct($sql) {
$this->sql = $sql; $this->sql = $sql;
$this->dump = false;
}
public function dump() {
$this->dump = true;
return $this;
} }
public abstract function execute(); public abstract function execute();

@ -2,6 +2,7 @@
namespace Driver\SQL\Query; namespace Driver\SQL\Query;
use Driver\SQL\Condition\CondOr;
use Driver\SQL\Join; use Driver\SQL\Join;
class Select extends Query { class Select extends Query {
@ -33,7 +34,7 @@ class Select extends Query {
} }
public function where(...$conditions) { public function where(...$conditions) {
$this->conditions = array_merge($this->conditions, $conditions); $this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions));
return $this; return $this;
} }

@ -2,6 +2,8 @@
namespace Driver\SQL\Query; namespace Driver\SQL\Query;
use Driver\SQL\Condition\CondOr;
class Update extends Query { class Update extends Query {
private array $values; private array $values;
@ -16,7 +18,7 @@ class Update extends Query {
} }
public function where(...$conditions) { public function where(...$conditions) {
$this->conditions = array_merge($this->conditions, $conditions); $this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions));
return $this; return $this;
} }

@ -144,6 +144,7 @@ abstract class SQL {
$returning = $this->getReturning($returningCol); $returning = $this->getReturning($returningCol);
$query = "INSERT INTO $tableName$columnStr VALUES$values$onDuplicateKey$returning"; $query = "INSERT INTO $tableName$columnStr VALUES$values$onDuplicateKey$returning";
if($insert->dump) { var_dump($query); var_dump($parameters); }
$res = $this->execute($query, $parameters, !empty($returning)); $res = $this->execute($query, $parameters, !empty($returning));
$success = ($res !== FALSE); $success = ($res !== FALSE);
@ -189,6 +190,7 @@ abstract class SQL {
$limit = ($select->getLimit() > 0 ? (" LIMIT " . $select->getLimit()) : ""); $limit = ($select->getLimit() > 0 ? (" LIMIT " . $select->getLimit()) : "");
$offset = ($select->getOffset() > 0 ? (" OFFSET " . $select->getOffset()) : ""); $offset = ($select->getOffset() > 0 ? (" OFFSET " . $select->getOffset()) : "");
$query = "SELECT $columns FROM $tables$joinStr$where$orderBy$limit$offset"; $query = "SELECT $columns FROM $tables$joinStr$where$orderBy$limit$offset";
if($select->dump) { var_dump($query); var_dump($params); }
return $this->execute($query, $params, true); return $this->execute($query, $params, true);
} }
@ -198,6 +200,7 @@ abstract class SQL {
$where = $this->getWhereClause($delete->getConditions(), $params); $where = $this->getWhereClause($delete->getConditions(), $params);
$query = "DELETE FROM $table$where"; $query = "DELETE FROM $table$where";
if($delete->dump) { var_dump($query); }
return $this->execute($query); return $this->execute($query);
} }
@ -218,6 +221,7 @@ abstract class SQL {
$where = $this->getWhereClause($update->getConditions(), $params); $where = $this->getWhereClause($update->getConditions(), $params);
$query = "UPDATE $table SET $valueStr$where"; $query = "UPDATE $table SET $valueStr$where";
if($update->dump) { var_dump($query); var_dump($params); }
return $this->execute($query, $params); return $this->execute($query, $params);
} }
@ -290,6 +294,7 @@ abstract class SQL {
protected abstract function execute($query, $values=NULL, $returnValues=false); protected abstract function execute($query, $values=NULL, $returnValues=false);
protected function buildCondition($condition, &$params) { protected function buildCondition($condition, &$params) {
if ($condition instanceof CondOr) { if ($condition instanceof CondOr) {
$conditions = array(); $conditions = array();
foreach($condition->getConditions() as $cond) { foreach($condition->getConditions() as $cond) {
@ -304,7 +309,7 @@ abstract class SQL {
} else if ($condition instanceof CondBool) { } else if ($condition instanceof CondBool) {
return $this->columnName($condition->getValue()); return $this->columnName($condition->getValue());
} else if (is_array($condition)) { } else if (is_array($condition)) {
if (count($condition) == 1) { if (count($condition) === 1) {
return $this->buildCondition($condition[0], $params); return $this->buildCondition($condition[0], $params);
} else { } else {
$conditions = array(); $conditions = array();

@ -2,8 +2,6 @@
namespace Elements; namespace Elements;
use View;
abstract class Body extends View { abstract class Body extends View {
public function __construct($document) { public function __construct($document) {
parent::__construct($document); parent::__construct($document);

@ -2,8 +2,6 @@
namespace Elements; namespace Elements;
use View;
abstract class Head extends View { abstract class Head extends View {
protected array $sources; protected array $sources;
@ -47,7 +45,7 @@ abstract class Head extends View {
public function addKeywords($keywords) { array_merge($this->keywords, $keywords); } public function addKeywords($keywords) { array_merge($this->keywords, $keywords); }
public function getTitle() { return $this->title; } public function getTitle() { return $this->title; }
public function addCSS($href, $type = Link::MIME_TEXT_CSS) { $this->sources[] = new Link("stylesheet", $href, $type); } public function addCSS($href, $type = Link::MIME_TEXT_CSS) { $this->sources[] = new Link(Link::STYLESHEET, $href, $type); }
public function addStyle($style) { $this->sources[] = new Style($style); } public function addStyle($style) { $this->sources[] = new Style($style); }
public function addJS($url) { $this->sources[] = new Script(Script::MIME_TEXT_JAVASCRIPT, $url, ""); } public function addJS($url) { $this->sources[] = new Script(Script::MIME_TEXT_JAVASCRIPT, $url, ""); }
public function addJSCode($code) { $this->sources[] = new Script(Script::MIME_TEXT_JAVASCRIPT, "", $code); } public function addJSCode($code) { $this->sources[] = new Script(Script::MIME_TEXT_JAVASCRIPT, "", $code); }

@ -2,28 +2,15 @@
namespace Elements; namespace Elements;
use View; class Link extends StaticView {
class Link extends View {
const STYLESHEET = "stylesheet"; const STYLESHEET = "stylesheet";
const MIME_TEXT_CSS = "text/css"; const MIME_TEXT_CSS = "text/css";
const FONTAWESOME = '/css/fontawesome.min.css'; const FONTAWESOME = "/css/fontawesome.min.css";
// const JQUERY_UI = '/css/jquery-ui.css'; const BOOTSTRAP = "/css/bootstrap.min.css";
// const JQUERY_TERMINAL = '/css/jquery.terminal.min.css';
const BOOTSTRAP = '/css/bootstrap.min.css';
// const BOOTSTRAP_THEME = '/css/bootstrap-theme.min.css';
// const BOOTSTRAP_DATEPICKER_CSS = '/css/bootstrap-datepicker.standalone.min.css';
// const BOOTSTRAP_DATEPICKER3_CSS = '/css/bootstrap-datepicker.standalone.min.css';
// const HIGHLIGHT = '/css/highlight.css';
// const HIGHLIGHT_THEME = '/css/theme.css';
const CORE = "/css/style.css"; const CORE = "/css/style.css";
const ADMIN = "/css/admin.css"; const ADMIN = "/css/admin.css";
// const HOME = "/css/home.css";
// const REVEALJS = "/css/reveal.css";
// const REVEALJS_THEME_MOON = "/css/reveal_moon.css";
// const REVEALJS_THEME_BLACK = "/css/reveal_black.css";
const ADMINLTE = "/css/adminlte.min.css"; const ADMINLTE = "/css/adminlte.min.css";
private string $type; private string $type;

@ -2,36 +2,15 @@
namespace Elements; namespace Elements;
class Script extends \View { class Script extends StaticView {
const MIME_TEXT_JAVASCRIPT = "text/javascript"; const MIME_TEXT_JAVASCRIPT = "text/javascript";
const CORE = "/js/script.js"; const CORE = "/js/script.js";
// const HOME = "/js/home.js";
const ADMIN = "/js/admin.js"; const ADMIN = "/js/admin.js";
// const SORTTABLE = "/js/sorttable.js";
const JQUERY = "/js/jquery.min.js"; const JQUERY = "/js/jquery.min.js";
// const JQUERY_UI = "/js/jquery-ui.js";
// const JQUERY_MASKED_INPUT = "/js/jquery.maskedinput.min.js";
// const JQUERY_CONTEXT_MENU = "/js/jquery.contextmenu.min.js";
// const JQUERY_TERMINAL = "/js/jquery.terminal.min.js";
// const JQUERY_TERMINAL_UNIX = "/js/unix_formatting.js";
// const JSCOLOR = "/js/jscolor.min.js";
// const SYNTAX_HIGHLIGHTER = "/js/syntaxhighlighter.js";
// const HIGHLIGHT = "/js/highlight.pack.js";
// const GOOGLE_CHARTS = "/js/loader.js";
// const BOOTSTRAP = "/js/bootstrap.min.js";
// const BOOTSTRAP_DATEPICKER_JS = "/js/bootstrap-datepicker.min.js";
// const POPPER = "/js/popper.min.js";
// const JSMPEG = "/js/jsmpeg.min.js";
// const MOMENT = "/js/moment.min.js";
// const CHART = "/js/chart.js";
// const REVEALJS = "/js/reveal.js";
// const REVEALJS_PLUGIN_NOTES = "/js/reveal_notes.js";
const INSTALL = "/js/install.js"; const INSTALL = "/js/install.js";
const BOOTSTRAP = "/js/bootstrap.bundle.min.js"; const BOOTSTRAP = "/js/bootstrap.bundle.min.js";
const HIGHLIGHT_JS_LOADER = "\$(document).ready(function(){\$('code').each(function(i, block) { hljs.highlightBlock(block); }); })";
const ADMINLTE = "/js/adminlte.min.js"; const ADMINLTE = "/js/adminlte.min.js";
private string $type; private string $type;

@ -0,0 +1,9 @@
<?php
namespace Elements;
abstract class StaticView {
public abstract function getCode();
}

@ -2,9 +2,7 @@
namespace Elements; namespace Elements;
use View; class Style extends StaticView {
class Style extends View {
private string $style; private string $style;

@ -1,8 +1,8 @@
<?php <?php
use Elements\Document; namespace Elements;
abstract class View { abstract class View extends StaticView {
private Document $document; private Document $document;
private bool $loadView; private bool $loadView;
@ -112,4 +112,9 @@ abstract class View {
$hidden = ($hidden?" hidden" : ""); $hidden = ($hidden?" hidden" : "");
return "<div class=\"alert alert-$type$hidden\" role=\"alert\"$id>$text</div>"; return "<div class=\"alert alert-$type$hidden\" role=\"alert\"$id>$text</div>";
} }
protected function createBadge($type, $text) {
$text = htmlspecialchars($text);
return "<span class=\"badge badge-$type\">$text</span>";
}
} }

@ -9,7 +9,8 @@ use External\JWT;
class Session extends ApiObject { class Session extends ApiObject {
const DURATION = 120; # in minutes
const DURATION = 60*24;
private ?int $sessionId; private ?int $sessionId;
private User $user; private User $user;
@ -91,13 +92,13 @@ class Session extends ApiObject {
$this->updateMetaData(); $this->updateMetaData();
$sql = $this->user->getSQL(); $sql = $this->user->getSQL();
$hours = Session::DURATION; $minutes = Session::DURATION;
$columns = array("expires", "user_id", "ipAddress", "os", "browser", "data", "stay_logged_in"); $columns = array("expires", "user_id", "ipAddress", "os", "browser", "data", "stay_logged_in");
$success = $sql $success = $sql
->insert("Session", $columns) ->insert("Session", $columns)
->addRow( ->addRow(
(new DateTime())->modify("+$hours hour"), (new DateTime())->modify("+$minutes minute"),
$this->user->getId(), $this->user->getId(),
$this->ipAddress, $this->ipAddress,
$this->os, $this->os,
@ -125,11 +126,11 @@ class Session extends ApiObject {
public function update() { public function update() {
$this->updateMetaData(); $this->updateMetaData();
$hours = Session::DURATION; $minutes = Session::DURATION;
$sql = $this->user->getSQL(); $sql = $this->user->getSQL();
return $sql->update("Session") return $sql->update("Session")
->set("Session.expires", (new DateTime())->modify("+$hours hour")) ->set("Session.expires", (new DateTime())->modify("+$minutes minute"))
->set("Session.ipAddress", $this->ipAddress) ->set("Session.ipAddress", $this->ipAddress)
->set("Session.os", $this->os) ->set("Session.os", $this->os)
->set("Session.browser", $this->browser) ->set("Session.browser", $this->browser)

@ -1,19 +1,24 @@
<?php <?php
namespace Views; namespace Views\Admin;
// Source: https://adminlte.io/themes/v3/ // Source: https://adminlte.io/themes/v3/
use Documents\Document404\Body404;
use Elements\Body; use Elements\Body;
use Elements\Script; use Elements\Script;
use Elements\View;
use Views\View404;
class AdminDashboard extends Body { class AdminDashboardBody extends Body {
private array $errorMessages; private array $errorMessages;
private array $notifications;
public function __construct($document) { public function __construct($document) {
parent::__construct($document); parent::__construct($document);
$this->errorMessages = array(); $this->errorMessages = array();
$this->notifications = array();
} }
private function getNotifications() : array { private function getNotifications() : array {
@ -49,8 +54,7 @@ class AdminDashboard extends Body {
$iconMail = $this->createIcon("envelope", "fas"); $iconMail = $this->createIcon("envelope", "fas");
// Notifications // Notifications
$notifications = $this->getNotifications(); $numNotifications = count($this->notifications);
$numNotifications = count($notifications);
if ($numNotifications === 0) { if ($numNotifications === 0) {
$notificationText = L("No new notifications"); $notificationText = L("No new notifications");
} else if($numNotifications === 1) { } else if($numNotifications === 1) {
@ -98,7 +102,7 @@ class AdminDashboard extends Body {
// Notifications // Notifications
$i = 0; $i = 0;
foreach($notifications as $notification) { foreach($this->notifications as $notification) {
$title = $notification["title"]; $title = $notification["title"];
$notificationId = $notification["uid"]; $notificationId = $notification["uid"];
@ -150,12 +154,17 @@ class AdminDashboard extends Body {
), ),
); );
$notificationCount = count($this->notifications);
if ($notificationCount > 0) {
$menuEntries["dashboard"]["badge"] = array("type" => "warning", "value" => $notificationCount);
}
$currentView = $_GET["view"] ?? "dashboard"; $currentView = $_GET["view"] ?? "dashboard";
$html = $html =
"<aside class=\"main-sidebar sidebar-dark-primary elevation-4\"> "<aside class=\"main-sidebar sidebar-dark-primary elevation-4\">
<!-- Brand Logo --> <!-- Brand Logo -->
<a href=\"index3.html\" class=\"brand-link\"> <a href=\"/admin\" class=\"brand-link\">
<img src=\"/img/web_base_logo.png\" alt=\"WebBase Logo\" class=\"brand-image img-circle elevation-3\" <img src=\"/img/web_base_logo.png\" alt=\"WebBase Logo\" class=\"brand-image img-circle elevation-3\"
style=\"opacity: .8\"> style=\"opacity: .8\">
<span class=\"brand-text font-weight-light\">WebBase</span> <span class=\"brand-text font-weight-light\">WebBase</span>
@ -172,11 +181,17 @@ class AdminDashboard extends Body {
$name = L($menuEntry["name"]); $name = L($menuEntry["name"]);
$icon = $this->createIcon($menuEntry["icon"], "fas", "nav-icon"); $icon = $this->createIcon($menuEntry["icon"], "fas", "nav-icon");
$active = ($currentView === $view) ? " active" : ""; $active = ($currentView === $view) ? " active" : "";
$badge = $menuEntry["badge"] ?? "";
if($badge) {
$badgeType = $badge["type"];
$badgeValue = $badge["value"];
$badge = "<span class=\"badge badge-$badgeType right\">$badgeValue</span>";
}
$html .= $html .=
"<li class=\"nav-item\"> "<li class=\"nav-item\">
<a href=\"?view=$view\" class=\"nav-link$active\"> <a href=\"?view=$view\" class=\"nav-link$active\">
$icon $icon<p>$name$badge</p>
<p>$name </p>
</a> </a>
</li>"; </li>";
} }
@ -190,32 +205,64 @@ class AdminDashboard extends Body {
return $html; return $html;
} }
private function getView() {
$views = array(
"dashboard" => Dashboard::class,
"users" => UserOverview::class,
"404" => View404::class,
);
$currentView = $_GET["view"] ?? "dashboard";
if (!isset($views[$currentView])) {
$currentView = "404";
}
$view = new $views[$currentView]($this->getDocument());
assert($view instanceof View);
$code = $view->getCode();
if ($view instanceof AdminView) {
$this->errorMessages = array_merge($this->errorMessages, $view->getErrorMessages());
}
return $code;
}
public function loadView() {
parent::loadView();
$head = $this->getDocument()->getHead();
$head->addJS(Script::BOOTSTRAP);
$head->loadAdminlte();
$this->notifications = $this->getNotifications();
}
private function getContent() { private function getContent() {
$this->getUsers(); $this->getUsers();
$view = $this->getView();
$html = "<div class=\"content-wrapper p-2\">"; $html = "<div class=\"content-wrapper p-2\">";
foreach($this->errorMessages as $errorMessage) { foreach($this->errorMessages as $errorMessage) {
$html .= $this->createErrorText($errorMessage); $html .= $this->createErrorText($errorMessage);
} }
$html .= $view;
$html .= "</div>"; $html .= "</div>";
return $html; return $html;
} }
public function getCode() { public function getCode() {
$html = parent::getCode();
$head = $this->getDocument()->getHead();
$head->addJS(Script::BOOTSTRAP);
$head->loadAdminlte();
$header = $this->getHeader(); $header = $this->getHeader();
$sidebar = $this->getSidebar(); $sidebar = $this->getSidebar();
$content = $this->getContent(); $content = $this->getContent();
$html = $html .=
"<!-- LICENSE: /docs/LICENSE_ADMINLTE --> "<!-- LICENSE: /docs/LICENSE_ADMINLTE -->
<body class=\"hold-transition sidebar-mini layout-fixed\"> <body class=\"hold-transition sidebar-mini layout-fixed\">
<div class=\"wrapper\"> <div class=\"wrapper\">

@ -0,0 +1,46 @@
<?php
namespace Views\Admin;
use Elements\Document;
use Elements\View;
class AdminView extends View {
protected array $errorMessages;
public function __construct(Document $document) {
parent::__construct($document);
$this->errorMessages = array();
}
public function getErrorMessages() {
return $this->errorMessages;
}
public function getCode() {
$html = parent::getCode();
$home = L("Home");
$html .=
"<div class=\"content-header\">
<div class=\"container-fluid\">
<div class=\"row mb-2\">
<div class=\"col-sm-6\">
<h1 class=\"m-0 text-dark\">$this->title</h1>
</div>
<div class=\"col-sm-6\">
<ol class=\"breadcrumb float-sm-right\">
<li class=\"breadcrumb-item\"><a href=\"/\">$home</a></li>
<li class=\"breadcrumb-item active\">$this->title</li>
</ol>
</div>
</div>
</div><!-- /.container-fluid -->
</div>";
return $html;
}
}

@ -0,0 +1,25 @@
<?php
namespace Views\Admin;
use Elements\Document;
class Dashboard extends AdminView {
public function __construct(Document $document) {
parent::__construct($document);
}
public function loadView() {
parent::loadView();
$this->title = L("Dashboard");
}
public function getCode() {
$html = parent::getCode();
return $html;
}
}

@ -0,0 +1,164 @@
<?php
namespace Views\Admin;
use DateTime;
use Elements\Document;
class UserOverview extends AdminView {
private array $users;
private int $page;
private int $pageCount;
public function __construct(Document $document) {
parent::__construct($document);
$this->users = array();
$this->pageCount = 0;
$this->page = 1;
}
public function loadView() {
parent::loadView();
$this->title = L("User Control");
$this->requestUsers();
}
private function requestUsers() {
if(isset($_GET["page"]) && is_numeric($_GET["page"])) {
$this->page = intval($_GET["page"]);
} else {
$this->page = 1;
}
$req = new \Api\User\Fetch($this->getDocument()->getUser());
if (!$req->execute(array("page" => $this->page))) {
$this->errorMessages[] = $req->getLastError();
} else {
$result = $req->getResult();
$this->users = $result["users"];
$this->pageCount = $result["pages"];
}
}
private function getGroups($groups) {
$badges = [];
foreach($groups as $groupId => $group) {
$badgeClass = "secondary";
if ($groupId === USER_GROUP_ADMIN) {
$badgeClass = "danger";
}
$badges[] = $this->createBadge($badgeClass, $group);
}
return implode("&nbsp;", $badges);
}
private function getPagination() {
$userPageNavigation = L("User page navigation");
$previousDisabled = ($this->page == 1 ? " disabled" : "");
$nextDisabled = ($this->page >= $this->pageCount ? " disabled" : "");
$html =
"<nav aria-label=\"$userPageNavigation\" id=\"userPageNavigation\">
<ul class=\"pagination p-2 m-0 justify-content-end\">
<li class=\"page-item$previousDisabled\"><a class=\"page-link\" href=\"#\">Previous</a></li>";
for($i = 1; $i <= $this->pageCount; $i++) {
$active = $i === $this->page ? " active" : "";
$html .=
"<li class=\"page-item$active\"><a class=\"page-link\" href=\"#\">$i</a></li>";
}
$html .=
"<li class=\"page-item$nextDisabled\"><a class=\"page-link\" href=\"#\">Next</a></li>
</ul>
</nav>";
return $html;
}
private function getUserRows() {
$dateFormat = L("Y/m/d");
$userRows = array();
foreach($this->users as $uid => $user) {
$name = $user["name"];
$email = $user["email"] ?? "";
$registeredAt = (new DateTime($user["created_at"]))->format($dateFormat);
$groups = $this->getGroups($user["groups"]);
$userRows[] =
"<tr data-id=\"$uid\">
<td>$name</td>
<td>$email</td>
<td>$groups</td>
<td>$registeredAt</td>
</tr>";
}
return implode("", $userRows);
}
public function getCode() {
$html = parent::getCode();
// Icons
$iconRefresh = $this->createIcon("sync");
// Locale
$users = L("Users");
$name = L("Name");
$email = L("Email");
$groups = L("Groups");
$registeredAt = L("Registered At");
// Content
$pagination = $this->getPagination();
$userRows = $this->getUserRows();
$html .=
"<div class=\"content\">
<div class=\"container-fluid\">
<div class=\"row\">
<div class=\"col-lg-12\">
<div class=\"card\">
<div class=\"card-header border-0\">
<h3 class=\"card-title\">$users</h3>
<div class=\"card-tools\">
<a href=\"#\" class=\"btn btn-tool btn-sm\" id=\"userTableRefresh\">
$iconRefresh
</a>
</div>
</div>
<div class=\"card-body table-responsive p-0\">
<table class=\"table table-striped table-valign-middle\" id=\"userTable\">
<thead>
<tr>
<th>$name</th>
<th>$email</th>
<th>$groups</th>
<th>$registeredAt</th>
</tr>
</thead>
<tbody>
$userRows
</tbody>
</table>
$pagination
</div>
</div>
</div>
</div>
</div>
</div>";
return $html;
}
}

@ -3,20 +3,26 @@
namespace Views; namespace Views;
use Api\GetLanguages; use Api\GetLanguages;
use Elements\View;
class LanguageFlags extends \View { class LanguageFlags extends View {
private array $languageFlags;
public function __construct($document) { public function __construct($document) {
parent::__construct($document); parent::__construct($document);
$this->languageFlags = array();
} }
public function getCode() { public function loadView() {
parent::loadView();
$request = new GetLanguages($this->getDocument()->getUser());
if($request->execute()) {
$requestUri = $_SERVER["REQUEST_URI"]; $requestUri = $_SERVER["REQUEST_URI"];
$queryString = $_SERVER['QUERY_STRING']; $queryString = $_SERVER['QUERY_STRING'];
$flags = array();
$request = new GetLanguages($this->getDocument()->getUser());
$params = explode("&", $queryString); $params = explode("&", $queryString);
$query = array(); $query = array();
foreach($params as $param) { foreach($params as $param) {
@ -33,22 +39,22 @@ class LanguageFlags extends \View {
} }
$url = parse_url($requestUri, PHP_URL_PATH) . "?"; $url = parse_url($requestUri, PHP_URL_PATH) . "?";
if($request->execute()) {
foreach($request->getResult()['languages'] as $lang) { foreach($request->getResult()['languages'] as $lang) {
$langCode = $lang['code']; $langCode = $lang['code'];
$langName = $lang['name']; $langName = $lang['name'];
$query['lang'] = $langCode; $query['lang'] = $langCode;
$queryString = http_build_query($query); $queryString = http_build_query($query);
$flags[] = $this->createLink( $this->languageFlags[] = $this->createLink(
"$url$queryString", "$url$queryString",
"<img class=\"p-1\" src=\"/img/icons/lang/$langCode.gif\" alt=\"$langName\" title=\"$langName\">" "<img class=\"p-1\" src=\"/img/icons/lang/$langCode.gif\" alt=\"$langName\" title=\"$langName\">"
); );
} }
} else { }
$flags[] = $this->createErrorText($request->getLastError());
} }
return implode('', $flags); public function getCode() {
return implode('', $this->languageFlags);
} }
} }

@ -10,9 +10,13 @@ class LoginBody extends Body {
parent::__construct($document); parent::__construct($document);
} }
public function getCode() { public function loadView() {
parent::loadView();
$this->getDocument()->getHead()->loadBootstrap(); $this->getDocument()->getHead()->loadBootstrap();
}
public function getCode() {
$html = parent::getCode();
$username = L("Username"); $username = L("Username");
$password = L("Password"); $password = L("Password");
@ -25,7 +29,7 @@ class LoginBody extends Body {
$domain = $_SERVER['HTTP_HOST']; $domain = $_SERVER['HTTP_HOST'];
$protocol = getProtocol(); $protocol = getProtocol();
$html = "<body>"; $html .= "<body>";
$accountCreated = ""; $accountCreated = "";
if(isset($_GET["accountCreated"])) { if(isset($_GET["accountCreated"])) {

@ -0,0 +1,13 @@
<?php
namespace Views;
use Elements\View;
class View404 extends View {
public function getCode() {
return parent::getCode() . "<b>Not found</b>";
}
};

@ -1,4 +1,6 @@
$(document).ready(function() { $(document).ready(function() {
// Login
$("#username").keypress(function(e) { if(e.which == 13) $("#password").focus(); }); $("#username").keypress(function(e) { if(e.which == 13) $("#password").focus(); });
$("#password").keypress(function(e) { if(e.which == 13) $("#btnLogin").click(); }); $("#password").keypress(function(e) { if(e.which == 13) $("#btnLogin").click(); });
$("#btnLogin").click(function() { $("#btnLogin").click(function() {
@ -24,8 +26,26 @@ $(document).ready(function() {
}); });
}); });
$("#toggleSidebar").click(function() { $("#userTableRefresh").click(function() {
$(".main-wrapper").toggleClass("sidebar-collapsed"); let tbody = $("#userTable > tbody");
$(".main-sidebar").toggleClass("collapsed"); let page = parseInt($("#userPageNavigation li.active > a").text().trim());
tbody.find("tr").remove();
tbody.append("<tr><td colspan=\"4\" class=\"text-center\">Loading… " + createLoadingIcon() + "</td></tr>");
jsCore.apiCall("/user/fetch", { page: page}, function (data) {
let pageCount = data["pages"];
let users = data["users"];
let userRows = [];
// TODO: .. maybe use ts instead of plain js?
for(let userId in users) {
let user = users[userId];
userRows.push("<tr><td>" + user.name + "</td><td>" + user.email + "</td><td></td><td></td></tr>");
}
tbody.html(userRows.join(""));
}, function (err) {
alert(err);
});
}); });
}); });

@ -274,3 +274,7 @@ let jsCore = new Core();
$(document).ready(function() { $(document).ready(function() {
}); });
function createLoadingIcon() {
return '<i class="fas fa-spin fa-spinner"></i>';
}

@ -10,7 +10,7 @@ class ApiTestCase(PhpTest):
# ApiKeys # ApiKeys
"Testing get api keys empty…": self.test_get_api_keys_empty, "Testing get api keys empty…": self.test_get_api_keys_empty,
"Testing create api key…": self.test_create_api_key, "Testing create api key…": self.test_create_api_key,
"Testing referesh api key…": self.test_refresh_api_key, "Testing refresh api key…": self.test_refresh_api_key,
"Testing revoke api key…": self.test_revoke_api_key, "Testing revoke api key…": self.test_revoke_api_key,
# Notifications # Notifications

@ -11,6 +11,7 @@ class InstallTestCase(PhpTest):
"Testing invalid usernames…": self.test_invalid_usernames, "Testing invalid usernames…": self.test_invalid_usernames,
"Testing invalid password…": self.test_invalid_password, "Testing invalid password…": self.test_invalid_password,
"Testing not matching password…": self.test_not_matching_passwords, "Testing not matching password…": self.test_not_matching_passwords,
"Testing invalid email…": self.test_invalid_email,
"Testing user creation…": self.test_create_user, "Testing user creation…": self.test_create_user,
"Testing skip mail configuration…": self.test_skil_mail_config, "Testing skip mail configuration…": self.test_skil_mail_config,
"Testing complete setup…": self.test_complete_setup, "Testing complete setup…": self.test_complete_setup,
@ -40,8 +41,13 @@ class InstallTestCase(PhpTest):
self.assertEquals(False, obj["success"]) self.assertEquals(False, obj["success"])
self.assertEquals("The given passwords do not match", obj["msg"]) self.assertEquals("The given passwords do not match", obj["msg"])
def test_invalid_email(self):
obj = self.httpPost(data={ "username": PhpTest.ADMIN_USERNAME, "password": PhpTest.ADMIN_PASSWORD, "confirmPassword": PhpTest.ADMIN_PASSWORD, "email": "123abc" })
self.assertEquals(False, obj["success"])
self.assertEquals("Invalid email address", obj["msg"])
def test_create_user(self): def test_create_user(self):
obj = self.httpPost(data={ "username": PhpTest.ADMIN_USERNAME, "password": PhpTest.ADMIN_PASSWORD, "confirmPassword": PhpTest.ADMIN_PASSWORD }) obj = self.httpPost(data={ "username": PhpTest.ADMIN_USERNAME, "password": PhpTest.ADMIN_PASSWORD, "confirmPassword": PhpTest.ADMIN_PASSWORD, "email": "test@test.com" })
self.assertEquals(True, obj["success"], obj["msg"]) self.assertEquals(True, obj["success"], obj["msg"])
def test_skil_mail_config(self): def test_skil_mail_config(self):