diff --git a/core/Api/User/Fetch.class.php b/core/Api/User/Fetch.class.php index ab75e47..e81c2fa 100644 --- a/core/Api/User/Fetch.class.php +++ b/core/Api/User/Fetch.class.php @@ -2,14 +2,38 @@ namespace Api\User; +use Api\Parameter\Parameter; use \Api\Request; class Fetch extends Request { + const SELECT_SIZE = 20; + + private int $userCount; + 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->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()) { @@ -17,11 +41,25 @@ class Fetch extends Request { return false; } + $page = $this->getParam("page"); + if($page < 1) { + return $this->createError("Invalid page count"); + } + + if (!$this->getUserCount()) { + return false; + } + $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") ->leftJoin("UserGroup", "User.uid", "UserGroup.user_id") ->leftJoin("Group", "Group.uid", "UserGroup.group_id") + ->orderBy("User.uid") + ->ascending() + ->limit(Fetch::SELECT_SIZE) + ->offset(($page - 1) * Fetch::SELECT_SIZE) ->execute(); $this->success = ($res !== FALSE); @@ -30,14 +68,15 @@ class Fetch extends Request { if($this->success) { $this->result["users"] = array(); foreach($res as $row) { - $userId = $row["userId"]; - $groupId = $row["groupId"]; + $userId = intval($row["userId"]); + $groupId = intval($row["groupId"]); $groupName = $row["groupName"]; if (!isset($this->result["users"][$userId])) { $this->result["users"][$userId] = array( "uid" => $userId, "name" => $row["name"], "email" => $row["email"], + "created_at" => $row["created_at"], "groups" => array(), ); } @@ -46,6 +85,7 @@ class Fetch extends Request { $this->result["users"][$userId]["groups"][$groupId] = $groupName; } } + $this->result["pages"] = intval(ceil($this->userCount / Fetch::SELECT_SIZE)); } return $this->success; diff --git a/core/Configuration/CreateDatabase.class.php b/core/Configuration/CreateDatabase.class.php index d3c66b7..463ed8e 100755 --- a/core/Configuration/CreateDatabase.class.php +++ b/core/Configuration/CreateDatabase.class.php @@ -31,6 +31,7 @@ class CreateDatabase { ->addString("salt", 16) ->addString("password", 64) ->addInt("language_id", true, 1) + ->addDateTime("registered_at", false, $sql->currentTimestamp()) ->primaryKey("uid") ->unique("email") ->unique("name") diff --git a/core/Documents/Admin.class.php b/core/Documents/Admin.class.php index e34601c..6386b41 100644 --- a/core/Documents/Admin.class.php +++ b/core/Documents/Admin.class.php @@ -5,12 +5,12 @@ namespace Documents { use Documents\Admin\AdminHead; use Elements\Document; use Objects\User; - use Views\AdminDashboard; + use Views\Admin\AdminDashboardBody; use Views\LoginBody; class Admin extends Document { 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); } } diff --git a/core/Documents/Document404.class.php b/core/Documents/Document404.class.php index 80a0950..298b7f1 100644 --- a/core/Documents/Document404.class.php +++ b/core/Documents/Document404.class.php @@ -17,6 +17,7 @@ namespace Documents\Document404 { use Elements\Body; use Elements\Head; + use Views\View404; class Head404 extends Head { @@ -25,11 +26,6 @@ namespace Documents\Document404 { } protected function initSources() { - // $this->loadJQuery(); - // $this->loadBootstrap(); - // $this->loadFontawesome(); - // $this->addJS(\Elements\Script::CORE); - // $this->addCSS(\Elements\Link::CORE); } protected function initMetas() { @@ -59,7 +55,7 @@ namespace Documents\Document404 { public function getCode() { $html = parent::getCode(); - $html .= "404 Not Found"; + $html .= "" . (new View404($this->getDocument())) . ""; return $html; } } diff --git a/core/Documents/Install.class.php b/core/Documents/Install.class.php index e1d0db6..a693c80 100644 --- a/core/Documents/Install.class.php +++ b/core/Documents/Install.class.php @@ -17,6 +17,7 @@ namespace Documents { namespace Documents\Install { use Api\Notifications\Create; + use Api\Parameter\Parameter; use Configuration\CreateDatabase; use Driver\SQL\SQL; use Elements\Body; @@ -289,8 +290,8 @@ namespace Documents\Install { $username = $this->getParameter("username"); $password = $this->getParameter("password"); $confirmPassword = $this->getParameter("confirmPassword"); + $email = $this->getParameter("email") ?? ""; - $msg = $this->errorString; $success = true; $missingInputs = array(); @@ -321,13 +322,16 @@ namespace Documents\Install { } else if(strlen($password) < 6) { $msg = "The password should be at least 6 characters long"; $success = false; + } else if($email && Parameter::parseType($email) !== Parameter::TYPE_EMAIL) { + $msg = "Invalid email address"; + $success = false; } else { $salt = generateRandomString(16); $hash = hash('sha256', $password . $salt); $sql = $user->getSQL(); - $success = $sql->insert("User", array("name", "salt", "password")) - ->addRow($username, $salt, $hash) + $success = $sql->insert("User", array("name", "salt", "password", "email")) + ->addRow($username, $salt, $hash, $email) ->returning("uid") ->execute() && $sql->insert("UserGroup", array("group_id", "user_id")) @@ -606,6 +610,7 @@ namespace Documents\Install { "title" => "Create a User", "form" => array( 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" => "Confirm Password", "name" => "confirmPassword", "type" => "password", "required" => true), ), @@ -704,7 +709,7 @@ namespace Documents\Install { $id = $button["id"]; $float = $button["float"]; $disabled = (isset($button["disabled"]) && $button["disabled"]) ? " disabled" : ""; - $button = ""; + $button = ""; if($float === "left") { $buttonsLeft .= $button; diff --git a/core/Driver/SQL/Condition/CondOr.class.php b/core/Driver/SQL/Condition/CondOr.class.php index 8d83b34..b92ab6b 100644 --- a/core/Driver/SQL/Condition/CondOr.class.php +++ b/core/Driver/SQL/Condition/CondOr.class.php @@ -7,7 +7,7 @@ class CondOr extends Condition { private array $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; } diff --git a/core/Driver/SQL/Query/Delete.class.php b/core/Driver/SQL/Query/Delete.class.php index 98d5232..705fbfe 100644 --- a/core/Driver/SQL/Query/Delete.class.php +++ b/core/Driver/SQL/Query/Delete.class.php @@ -2,6 +2,8 @@ namespace Driver\SQL\Query; +use Driver\SQL\Condition\CondOr; + class Delete extends Query { private string $table; @@ -14,7 +16,7 @@ class Delete extends Query { } public function where(...$conditions) { - $this->conditions = array_merge($this->conditions, $conditions); + $this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions)); return $this; } diff --git a/core/Driver/SQL/Query/Query.class.php b/core/Driver/SQL/Query/Query.class.php index 7d410e6..b58a3b7 100644 --- a/core/Driver/SQL/Query/Query.class.php +++ b/core/Driver/SQL/Query/Query.class.php @@ -7,9 +7,16 @@ use Driver\SQL\SQL; abstract class Query { protected SQL $sql; + public bool $dump; public function __construct($sql) { $this->sql = $sql; + $this->dump = false; + } + + public function dump() { + $this->dump = true; + return $this; } public abstract function execute(); diff --git a/core/Driver/SQL/Query/Select.class.php b/core/Driver/SQL/Query/Select.class.php index e3255eb..116fca7 100644 --- a/core/Driver/SQL/Query/Select.class.php +++ b/core/Driver/SQL/Query/Select.class.php @@ -2,6 +2,7 @@ namespace Driver\SQL\Query; +use Driver\SQL\Condition\CondOr; use Driver\SQL\Join; class Select extends Query { @@ -33,7 +34,7 @@ class Select extends Query { } public function where(...$conditions) { - $this->conditions = array_merge($this->conditions, $conditions); + $this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions)); return $this; } diff --git a/core/Driver/SQL/Query/Update.class.php b/core/Driver/SQL/Query/Update.class.php index 898f56f..e69a4f3 100644 --- a/core/Driver/SQL/Query/Update.class.php +++ b/core/Driver/SQL/Query/Update.class.php @@ -2,6 +2,8 @@ namespace Driver\SQL\Query; +use Driver\SQL\Condition\CondOr; + class Update extends Query { private array $values; @@ -16,7 +18,7 @@ class Update extends Query { } public function where(...$conditions) { - $this->conditions = array_merge($this->conditions, $conditions); + $this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions)); return $this; } diff --git a/core/Driver/SQL/SQL.class.php b/core/Driver/SQL/SQL.class.php index d6edac9..5854df8 100644 --- a/core/Driver/SQL/SQL.class.php +++ b/core/Driver/SQL/SQL.class.php @@ -144,6 +144,7 @@ abstract class SQL { $returning = $this->getReturning($returningCol); $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)); $success = ($res !== FALSE); @@ -189,6 +190,7 @@ abstract class SQL { $limit = ($select->getLimit() > 0 ? (" LIMIT " . $select->getLimit()) : ""); $offset = ($select->getOffset() > 0 ? (" OFFSET " . $select->getOffset()) : ""); $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); } @@ -198,6 +200,7 @@ abstract class SQL { $where = $this->getWhereClause($delete->getConditions(), $params); $query = "DELETE FROM $table$where"; + if($delete->dump) { var_dump($query); } return $this->execute($query); } @@ -218,6 +221,7 @@ abstract class SQL { $where = $this->getWhereClause($update->getConditions(), $params); $query = "UPDATE $table SET $valueStr$where"; + if($update->dump) { var_dump($query); var_dump($params); } return $this->execute($query, $params); } @@ -290,6 +294,7 @@ abstract class SQL { protected abstract function execute($query, $values=NULL, $returnValues=false); protected function buildCondition($condition, &$params) { + if ($condition instanceof CondOr) { $conditions = array(); foreach($condition->getConditions() as $cond) { @@ -304,7 +309,7 @@ abstract class SQL { } else if ($condition instanceof CondBool) { return $this->columnName($condition->getValue()); } else if (is_array($condition)) { - if (count($condition) == 1) { + if (count($condition) === 1) { return $this->buildCondition($condition[0], $params); } else { $conditions = array(); diff --git a/core/Elements/Body.class.php b/core/Elements/Body.class.php index 3e1f66a..196961a 100644 --- a/core/Elements/Body.class.php +++ b/core/Elements/Body.class.php @@ -2,8 +2,6 @@ namespace Elements; -use View; - abstract class Body extends View { public function __construct($document) { parent::__construct($document); diff --git a/core/Elements/Head.class.php b/core/Elements/Head.class.php index 0ac7391..ac071ab 100644 --- a/core/Elements/Head.class.php +++ b/core/Elements/Head.class.php @@ -2,8 +2,6 @@ namespace Elements; -use View; - abstract class Head extends View { protected array $sources; @@ -47,7 +45,7 @@ abstract class Head extends View { public function addKeywords($keywords) { array_merge($this->keywords, $keywords); } 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 addJS($url) { $this->sources[] = new Script(Script::MIME_TEXT_JAVASCRIPT, $url, ""); } public function addJSCode($code) { $this->sources[] = new Script(Script::MIME_TEXT_JAVASCRIPT, "", $code); } diff --git a/core/Elements/Link.class.php b/core/Elements/Link.class.php index 25a8105..3e06cce 100644 --- a/core/Elements/Link.class.php +++ b/core/Elements/Link.class.php @@ -2,29 +2,16 @@ namespace Elements; -use View; - -class Link extends View { +class Link extends StaticView { const STYLESHEET = "stylesheet"; const MIME_TEXT_CSS = "text/css"; - const FONTAWESOME = '/css/fontawesome.min.css'; - // const JQUERY_UI = '/css/jquery-ui.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 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 FONTAWESOME = "/css/fontawesome.min.css"; + const BOOTSTRAP = "/css/bootstrap.min.css"; + const CORE = "/css/style.css"; + const ADMIN = "/css/admin.css"; + const ADMINLTE = "/css/adminlte.min.css"; private string $type; private string $rel; diff --git a/core/Elements/Script.class.php b/core/Elements/Script.class.php index 5e9224a..b924f7a 100644 --- a/core/Elements/Script.class.php +++ b/core/Elements/Script.class.php @@ -2,37 +2,16 @@ namespace Elements; -class Script extends \View { +class Script extends StaticView { const MIME_TEXT_JAVASCRIPT = "text/javascript"; - const CORE = "/js/script.js"; - // const HOME = "/js/home.js"; - const ADMIN = "/js/admin.js"; - // const SORTTABLE = "/js/sorttable.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 CORE = "/js/script.js"; + const ADMIN = "/js/admin.js"; + const JQUERY = "/js/jquery.min.js"; + const INSTALL = "/js/install.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 $content; diff --git a/core/Elements/StaticView.class.php b/core/Elements/StaticView.class.php new file mode 100644 index 0000000..c207f50 --- /dev/null +++ b/core/Elements/StaticView.class.php @@ -0,0 +1,9 @@ +$text"; } + + protected function createBadge($type, $text) { + $text = htmlspecialchars($text); + return "$text"; + } } \ No newline at end of file diff --git a/core/Objects/Session.class.php b/core/Objects/Session.class.php index 45cbc86..4081293 100644 --- a/core/Objects/Session.class.php +++ b/core/Objects/Session.class.php @@ -9,7 +9,8 @@ use External\JWT; class Session extends ApiObject { - const DURATION = 120; + # in minutes + const DURATION = 60*24; private ?int $sessionId; private User $user; @@ -91,13 +92,13 @@ class Session extends ApiObject { $this->updateMetaData(); $sql = $this->user->getSQL(); - $hours = Session::DURATION; + $minutes = Session::DURATION; $columns = array("expires", "user_id", "ipAddress", "os", "browser", "data", "stay_logged_in"); $success = $sql ->insert("Session", $columns) ->addRow( - (new DateTime())->modify("+$hours hour"), + (new DateTime())->modify("+$minutes minute"), $this->user->getId(), $this->ipAddress, $this->os, @@ -125,11 +126,11 @@ class Session extends ApiObject { public function update() { $this->updateMetaData(); - $hours = Session::DURATION; + $minutes = Session::DURATION; $sql = $this->user->getSQL(); 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.os", $this->os) ->set("Session.browser", $this->browser) diff --git a/core/Views/AdminDashboard.class.php b/core/Views/Admin/AdminDashboardBody.class.php similarity index 78% rename from core/Views/AdminDashboard.class.php rename to core/Views/Admin/AdminDashboardBody.class.php index 24843cd..c705149 100644 --- a/core/Views/AdminDashboard.class.php +++ b/core/Views/Admin/AdminDashboardBody.class.php @@ -1,19 +1,24 @@ errorMessages = array(); + $this->notifications = array(); } private function getNotifications() : array { @@ -49,8 +54,7 @@ class AdminDashboard extends Body { $iconMail = $this->createIcon("envelope", "fas"); // Notifications - $notifications = $this->getNotifications(); - $numNotifications = count($notifications); + $numNotifications = count($this->notifications); if ($numNotifications === 0) { $notificationText = L("No new notifications"); } else if($numNotifications === 1) { @@ -98,7 +102,7 @@ class AdminDashboard extends Body { // Notifications $i = 0; - foreach($notifications as $notification) { + foreach($this->notifications as $notification) { $title = $notification["title"]; $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"; $html = ""; @@ -190,32 +205,64 @@ class AdminDashboard extends Body { 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() { $this->getUsers(); + $view = $this->getView(); $html = "
"; foreach($this->errorMessages as $errorMessage) { $html .= $this->createErrorText($errorMessage); } + $html .= $view; $html .= "
"; - return $html; } public function getCode() { - - $head = $this->getDocument()->getHead(); - $head->addJS(Script::BOOTSTRAP); - $head->loadAdminlte(); + $html = parent::getCode(); $header = $this->getHeader(); $sidebar = $this->getSidebar(); $content = $this->getContent(); - $html = + $html .= "
diff --git a/core/Views/Admin/AdminView.class.php b/core/Views/Admin/AdminView.class.php new file mode 100644 index 0000000..f1ef92b --- /dev/null +++ b/core/Views/Admin/AdminView.class.php @@ -0,0 +1,46 @@ +errorMessages = array(); + } + + public function getErrorMessages() { + return $this->errorMessages; + } + + public function getCode() { + $html = parent::getCode(); + + $home = L("Home"); + + $html .= + "
+
+
+
+

$this->title

+
+
+
    +
  1. $home
  2. +
  3. $this->title
  4. +
+
+
+
+
"; + + return $html; + } +} \ No newline at end of file diff --git a/core/Views/Admin/Dashboard.class.php b/core/Views/Admin/Dashboard.class.php new file mode 100644 index 0000000..43d924b --- /dev/null +++ b/core/Views/Admin/Dashboard.class.php @@ -0,0 +1,25 @@ +title = L("Dashboard"); + } + + public function getCode() { + $html = parent::getCode(); + + return $html; + } + +} \ No newline at end of file diff --git a/core/Views/Admin/UserOverview.class.php b/core/Views/Admin/UserOverview.class.php new file mode 100644 index 0000000..77da258 --- /dev/null +++ b/core/Views/Admin/UserOverview.class.php @@ -0,0 +1,164 @@ +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(" ", $badges); + } + + private function getPagination() { + + $userPageNavigation = L("User page navigation"); + $previousDisabled = ($this->page == 1 ? " disabled" : ""); + $nextDisabled = ($this->page >= $this->pageCount ? " disabled" : ""); + + $html = + ""; + + 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[] = + " + $name + $email + $groups + $registeredAt + "; + } + + 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 .= + "
+
+
+
+
+
+

$users

+ +
+
+ + + + + + + + + + + $userRows + +
$name$email$groups$registeredAt
+ $pagination +
+
+
+
+
+
"; + + return $html; + } +} \ No newline at end of file diff --git a/core/Views/LanguageFlags.class.php b/core/Views/LanguageFlags.class.php index 72f5e87..c4f0283 100644 --- a/core/Views/LanguageFlags.class.php +++ b/core/Views/LanguageFlags.class.php @@ -3,52 +3,58 @@ namespace Views; use Api\GetLanguages; +use Elements\View; -class LanguageFlags extends \View { +class LanguageFlags extends View { + + private array $languageFlags; public function __construct($document) { parent::__construct($document); + $this->languageFlags = array(); } - public function getCode() { + public function loadView() { + parent::loadView(); - $requestUri = $_SERVER["REQUEST_URI"]; - $queryString = $_SERVER['QUERY_STRING']; - - $flags = array(); $request = new GetLanguages($this->getDocument()->getUser()); - $params = explode("&", $queryString); - $query = array(); - foreach($params as $param) { - $aParam = explode("=", $param); - $key = $aParam[0]; - - if($key == "s" && startsWith($requestUri, "/s/")) - continue; - - $val = (isset($aParam[1]) ? $aParam[1] : ""); - if(!empty($key)) { - $query[$key] = $val; - } - } - - $url = parse_url($requestUri, PHP_URL_PATH) . "?"; if($request->execute()) { + + $requestUri = $_SERVER["REQUEST_URI"]; + $queryString = $_SERVER['QUERY_STRING']; + + $params = explode("&", $queryString); + $query = array(); + foreach($params as $param) { + $aParam = explode("=", $param); + $key = $aParam[0]; + + if($key == "s" && startsWith($requestUri, "/s/")) + continue; + + $val = (isset($aParam[1]) ? $aParam[1] : ""); + if(!empty($key)) { + $query[$key] = $val; + } + } + + $url = parse_url($requestUri, PHP_URL_PATH) . "?"; + foreach($request->getResult()['languages'] as $lang) { $langCode = $lang['code']; $langName = $lang['name']; $query['lang'] = $langCode; $queryString = http_build_query($query); - $flags[] = $this->createLink( + $this->languageFlags[] = $this->createLink( "$url$queryString", "\"$langName\"" ); } - } else { - $flags[] = $this->createErrorText($request->getLastError()); } + } - return implode('', $flags); + public function getCode() { + return implode('', $this->languageFlags); } } \ No newline at end of file diff --git a/core/Views/LoginBody.class.php b/core/Views/LoginBody.class.php index be5477f..18415c5 100644 --- a/core/Views/LoginBody.class.php +++ b/core/Views/LoginBody.class.php @@ -10,9 +10,13 @@ class LoginBody extends Body { parent::__construct($document); } - public function getCode() { - + public function loadView() { + parent::loadView(); $this->getDocument()->getHead()->loadBootstrap(); + } + + public function getCode() { + $html = parent::getCode(); $username = L("Username"); $password = L("Password"); @@ -25,7 +29,7 @@ class LoginBody extends Body { $domain = $_SERVER['HTTP_HOST']; $protocol = getProtocol(); - $html = ""; + $html .= ""; $accountCreated = ""; if(isset($_GET["accountCreated"])) { diff --git a/core/Views/View404.class.php b/core/Views/View404.class.php new file mode 100644 index 0000000..7f972d2 --- /dev/null +++ b/core/Views/View404.class.php @@ -0,0 +1,13 @@ +Not found"; + } + +}; \ No newline at end of file diff --git a/js/admin.js b/js/admin.js index 69164e3..13a462b 100644 --- a/js/admin.js +++ b/js/admin.js @@ -1,4 +1,6 @@ $(document).ready(function() { + + // Login $("#username").keypress(function(e) { if(e.which == 13) $("#password").focus(); }); $("#password").keypress(function(e) { if(e.which == 13) $("#btnLogin").click(); }); $("#btnLogin").click(function() { @@ -24,8 +26,26 @@ $(document).ready(function() { }); }); - $("#toggleSidebar").click(function() { - $(".main-wrapper").toggleClass("sidebar-collapsed"); - $(".main-sidebar").toggleClass("collapsed"); + $("#userTableRefresh").click(function() { + let tbody = $("#userTable > tbody"); + let page = parseInt($("#userPageNavigation li.active > a").text().trim()); + tbody.find("tr").remove(); + tbody.append("Loading… " + createLoadingIcon() + ""); + + 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("" + user.name + "" + user.email + ""); + } + + tbody.html(userRows.join("")); + }, function (err) { + alert(err); + }); }); }); diff --git a/js/script.js b/js/script.js index 3033d32..8fb4939 100644 --- a/js/script.js +++ b/js/script.js @@ -274,3 +274,7 @@ let jsCore = new Core(); $(document).ready(function() { }); + +function createLoadingIcon() { + return ''; +} \ No newline at end of file diff --git a/test/apiTest.py b/test/apiTest.py index fc324a0..6ee848f 100644 --- a/test/apiTest.py +++ b/test/apiTest.py @@ -10,7 +10,7 @@ class ApiTestCase(PhpTest): # ApiKeys "Testing get api keys empty…": self.test_get_api_keys_empty, "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, # Notifications diff --git a/test/installTest.py b/test/installTest.py index 80ce7da..7c6da71 100644 --- a/test/installTest.py +++ b/test/installTest.py @@ -11,6 +11,7 @@ class InstallTestCase(PhpTest): "Testing invalid usernames…": self.test_invalid_usernames, "Testing invalid password…": self.test_invalid_password, "Testing not matching password…": self.test_not_matching_passwords, + "Testing invalid email…": self.test_invalid_email, "Testing user creation…": self.test_create_user, "Testing skip mail configuration…": self.test_skil_mail_config, "Testing complete setup…": self.test_complete_setup, @@ -40,8 +41,13 @@ class InstallTestCase(PhpTest): self.assertEquals(False, obj["success"]) 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): - 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"]) def test_skil_mail_config(self):