From d7a5897fc98560043d9e7ceb020dbeefaaceaf5a Mon Sep 17 00:00:00 2001 From: Roman Hergenreder Date: Thu, 2 Apr 2020 21:19:06 +0200 Subject: [PATCH] Notifications --- .../Create.class.php} | 5 +- .../Fetch.class.php} | 5 +- .../Refresh.class.php} | 5 +- .../Revoke.class.php} | 5 +- core/Api/Notifications/Create.class.php | 135 ++++++++++++ core/Api/Notifications/Fetch.class.php | 92 ++++++++ core/Api/{ => User}/Login.class.php | 9 +- core/Api/{ => User}/Logout.class.php | 4 +- core/Configuration/CreateDatabase.class.php | 27 ++- core/Configuration/database.sql | 84 ------- core/Documents/Admin.class.php | 2 +- core/Documents/Install.class.php | 22 +- core/Driver/SQL/MySQL.class.php | 70 ++---- core/Driver/SQL/PostgreSQL.class.php | 52 +---- core/Driver/SQL/SQL.class.php | 40 +++- core/External/JWT.class.php | 28 +-- core/Objects/User.class.php | 11 +- core/Views/Admin.class.php | 47 ++++ core/Views/Login.class.php | 2 - css/admin.css | 208 ++---------------- index.php | 15 +- js/admin.js | 2 +- js/install.js | 2 +- test/apiTest.py | 26 ++- 24 files changed, 469 insertions(+), 429 deletions(-) rename core/Api/{CreateApiKey.class.php => ApiKey/Create.class.php} (93%) rename core/Api/{GetApiKeys.class.php => ApiKey/Fetch.class.php} (93%) rename core/Api/{RefreshApiKey.class.php => ApiKey/Refresh.class.php} (95%) rename core/Api/{RevokeApiKey.class.php => ApiKey/Revoke.class.php} (95%) create mode 100644 core/Api/Notifications/Create.class.php create mode 100644 core/Api/Notifications/Fetch.class.php rename core/Api/{ => User}/Login.class.php (94%) rename core/Api/{ => User}/Logout.class.php (92%) delete mode 100644 core/Configuration/database.sql create mode 100644 core/Views/Admin.class.php diff --git a/core/Api/CreateApiKey.class.php b/core/Api/ApiKey/Create.class.php similarity index 93% rename from core/Api/CreateApiKey.class.php rename to core/Api/ApiKey/Create.class.php index 90d6a90..916bd19 100644 --- a/core/Api/CreateApiKey.class.php +++ b/core/Api/ApiKey/Create.class.php @@ -1,8 +1,9 @@ new Parameter('groupId', Parameter::TYPE_INT, true), + 'userId' => new Parameter('userId', Parameter::TYPE_INT, true), + 'title' => new StringType('title', 32), + 'message' => new StringType('message', 256), + )); + $this->isPublic = false; + } + + private function checkUser($userId) { + $sql = $this->user->getSQL(); + $res = $sql->select($sql->count()) + ->from("User") + ->where(new Compare("uid", $userId)) + ->execute(); + + $this->success = ($res !== FALSE); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + if ($res[0]["count"] == 0) { + $this->success = false; + $this->lastError = "User not found"; + } + } + + return $this->success; + } + + private function insertUserNotification($userId, $notificationId) { + $sql = $this->user->getSQL(); + $res = $sql->insert("UserNotification", array("user_id", "notification_id")) + ->addRow($userId, $notificationId) + ->execute(); + + $this->success = ($res !== FALSE); + $this->lastError = $sql->getLastError(); + return $this->success; + } + + private function checkGroup($groupId) { + $sql = $this->user->getSQL(); + $res = $sql->select($sql->count()) + ->from("Group") + ->where(new Compare("uid", $groupId)) + ->execute(); + + $this->success = ($res !== FALSE); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + if ($res[0]["count"] == 0) { + $this->success = false; + $this->lastError = "Group not found"; + } + } + + return $this->success; + } + + private function insertGroupNotification($groupId, $notificationId) { + $sql = $this->user->getSQL(); + $res = $sql->insert("GroupNotification", array("group_id", "notification_id")) + ->addRow($groupId, $notificationId) + ->execute(); + + $this->success = ($res !== FALSE); + $this->lastError = $sql->getLastError(); + return $this->success; + } + + private function createNotification($title, $message) { + $sql = $this->user->getSQL(); + $res = $sql->insert("Notification", array("title", "message")) + ->addRow($title, $message) + ->returning("uid") + ->execute(); + + $this->success = ($res !== FALSE); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + return $sql->getLastInsertId(); + } + + return $this->success; + } + + public function execute($values = array()) { + if(!parent::execute($values)) { + return false; + } + + $userId = $this->getParam("userId"); + $groupId = $this->getParam("groupId"); + $title = $this->getParam("title"); + $message = $this->getParam("message"); + + if (is_null($userId) && is_null($groupId)) { + return $this->createError("Either userId or groupId must be specified."); + } else if(!is_null($userId) && !is_null($groupId)) { + return $this->createError("Only one of userId and groupId must be specified."); + } else if(!is_null($userId)) { + if ($this->checkUser($userId)) { + $id = $this->createNotification($title, $message); + if ($this->success) { + return $this->insertUserNotification($userId, $id); + } + } + } else if(!is_null($groupId)) { + if ($this->checkGroup($groupId)) { + $id = $this->createNotification($title, $message); + if ($this->success) { + return $this->insertGroupNotification($groupId, $id); + } + } + } + + return $this->success; + } +}; + +?> diff --git a/core/Api/Notifications/Fetch.class.php b/core/Api/Notifications/Fetch.class.php new file mode 100644 index 0000000..79234b7 --- /dev/null +++ b/core/Api/Notifications/Fetch.class.php @@ -0,0 +1,92 @@ +loginRequired = true; + } + + private function fetchUserNotifications() { + $userId = $this->user->getId(); + $sql = $this->user->getSQL(); + $res = $sql->select($sql->distinct("Notification.uid"), "created_at", "title", "message") + ->from("Notification") + ->innerJoin("UserNotification", "UserNotification.notification_id", "Notification.uid") + ->where(new Compare("UserNotification.user_id", $userId)) + ->where(new Compare("UserNotification.seen", false)) + ->execute(); + + $this->success = ($res !== FALSE); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + foreach($res as $row) { + $id = $row["uid"]; + if (!isset($this->notifications[$id])) { + $this->notifications[$id] = array( + "uid" => $id, + "title" => $row["title"], + "message" => $row["message"], + "created_at" => $row["created_at"], + ); + } + } + } + + return $this->success; + } + + private function fetchGroupNotifications() { + $userId = $this->user->getId(); + $sql = $this->user->getSQL(); + $res = $sql->select($sql->distinct("Notification.uid"), "created_at", "title", "message") + ->from("Notification") + ->innerJoin("GroupNotification", "GroupNotification.notification_id", "Notification.uid") + ->innerJoin("UserGroup", "GroupNotification.group_id", "UserGroup.group_id") + ->where(new Compare("UserGroup.user_id", $userId)) + ->where(new Compare("GroupNotification.seen", false)) + ->execute(); + + $this->success = ($res !== FALSE); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + foreach($res as $row) { + $id = $row["uid"]; + if (!isset($this->notifications[$id])) { + $this->notifications[$id] = array( + "uid" => $id, + "title" => $row["title"], + "message" => $row["message"], + "created_at" => $row["created_at"], + ); + } + } + } + + return $this->success; + } + + public function execute($values = array()) { + if(!parent::execute($values)) { + return false; + } + + $this->notifications = array(); + if ($this->fetchUserNotifications() && $this->fetchGroupNotifications()) { + $this->result["notifications"] = $this->notifications; + } + + return $this->success; + } +}; + +?> diff --git a/core/Api/Login.class.php b/core/Api/User/Login.class.php similarity index 94% rename from core/Api/Login.class.php rename to core/Api/User/Login.class.php index ea4b412..64cf253 100644 --- a/core/Api/Login.class.php +++ b/core/Api/User/Login.class.php @@ -1,10 +1,11 @@ unique("name"); $queries[] = $sql->insert("Group", array("uid", "name")) - ->addRow(1, "Default") - ->addRow(2, "Administrator"); + ->addRow(USER_GROUP_DEFAULT, "Default") + ->addRow(USER_GROUP_ADMIN, "Administrator"); $queries[] = $sql->createTable("UserGroup") ->addInt("user_id") @@ -76,6 +76,29 @@ class CreateDatabase { ->foreignKey("user_id", "User", "uid") ->foreignKey("group_id", "Group", "uid"); + $queries[] = $sql->createTable("Notification") + ->addSerial("uid") + ->addDateTime("created_at", false, $sql->currentTimestamp()) + ->addString("title", 32) + ->addString("message", 256) + ->primaryKey("uid"); + + $queries[] = $sql->createTable("UserNotification") + ->addInt("user_id") + ->addInt("notification_id") + ->addBool("seen") + ->foreignKey("user_id", "User", "uid") + ->foreignKey("notification_id", "Notification", "uid") + ->unique("user_id", "notification_id"); + + $queries[] = $sql->createTable("GroupNotification") + ->addInt("group_id") + ->addInt("notification_id") + ->addBool("seen") + ->foreignKey("group_id", "Group", "uid") + ->foreignKey("notification_id", "Notification", "uid") + ->unique("group_id", "notification_id"); + $queries[] = $sql->createTable("ApiKey") ->addSerial("uid") ->addInt("user_id") diff --git a/core/Configuration/database.sql b/core/Configuration/database.sql deleted file mode 100644 index b4ce6d4..0000000 --- a/core/Configuration/database.sql +++ /dev/null @@ -1,84 +0,0 @@ --- --- API --- -CREATE TABLE IF NOT EXISTS Language ( - `uid` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT, - `code` VARCHAR(5) UNIQUE NOT NULL, - `name` VARCHAR(32) UNIQUE NOT NULL -); - -INSERT INTO Language (`uid`, `code`, `name`) VALUES - (1, 'en_US', 'American English'), - (2, 'de_DE', 'Deutsch Standard') - ON DUPLICATE KEY UPDATE name=name; - -CREATE TABLE IF NOT EXISTS User ( - `uid` INTEGER NOT NULL AUTO_INCREMENT, - `email` VARCHAR(64) UNIQUE DEFAULT NULL, - `name` VARCHAR(32) UNIQUE NOT NULL, - `salt` varchar(16) NOT NULL, - `password` varchar(64) NOT NULL, - `language_id` int(11) DEFAULT 1, - PRIMARY KEY (`uid`), - FOREIGN KEY (`language_id`) REFERENCES `Language` (`uid`) ON DELETE SET NULL -); - -CREATE TABLE IF NOT EXISTS UserInvitation ( - `email` VARCHAR(64) NOT NULL, - `token` VARCHAR(36) UNIQUE NOT NULL, - `valid_until` DATETIME NOT NULL -); - -CREATE TABLE IF NOT EXISTS UserToken ( - `user_id` INTEGER NOT NULL, - `token` VARCHAR(36) NOT NULL, - `type` ENUM('password_reset', 'confirmation') NOT NULL, - `valid_until` DATETIME NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `User` (`uid`) ON DELETE CASCADE -); - -CREATE TABLE IF NOT EXISTS `Group` ( - `gid` INTEGER NOT NULL AUTO_INCREMENT, - `name` VARCHAR(32) NOT NULL, - PRIMARY KEY (`gid`), - UNIQUE (`name`) -); - -INSERT INTO `Group` (gid, name) VALUES (1, "Default"), (2, "Administrator") - ON DUPLICATE KEY UPDATE name=name; - -CREATE TABLE IF NOT EXISTS UserGroup ( - `uid` INTEGER NOT NULL, - `gid` INTEGER NOT NULL, - UNIQUE (`uid`, `gid`), - FOREIGN KEY (`uid`) REFERENCES `User` (`uid`), - FOREIGN KEY (`gid`) REFERENCES `Group` (`gid`) -); - -CREATE TABLE IF NOT EXISTS Session ( - `uid` int(11) NOT NULL AUTO_INCREMENT, - `expires` timestamp NOT NULL, - `user_id` int(11) NOT NULL, - `ipAddress` varchar(45) NOT NULL, - `os` varchar(64) NOT NULL, - `browser` varchar(64) NOT NULL, - `data` JSON NOT NULL DEFAULT '{}', - `stay_logged_in` BOOLEAN DEFAULT TRUE, - PRIMARY KEY (`uid`), - FOREIGN KEY (`user_id`) REFERENCES `User` (`uid`) ON DELETE CASCADE -); - -CREATE TABLE IF NOT EXISTS ApiKey ( - `uid` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) NOT NULL, - `api_key` VARCHAR(64) NOT NULL, - `valid_until` DATETIME NOT NULL, - PRIMARY KEY (`uid`), - FOREIGN KEY (`user_id`) REFERENCES `User` (`uid`) -); - -CREATE TABLE IF NOT EXISTS ExternalSiteCache ( - `url` VARCHAR(256) UNIQUE, - `data` TEXT NOT NULL, - `expires` DATETIME DEFAULT NULL, -); diff --git a/core/Documents/Admin.class.php b/core/Documents/Admin.class.php index 30fb083..79ce7bc 100644 --- a/core/Documents/Admin.class.php +++ b/core/Documents/Admin.class.php @@ -58,7 +58,7 @@ namespace Documents\Admin { if(!$document->getUser()->isLoggedIn()) { $html .= new \Views\Login($document); } else { - $html .= "You are logged in :]"; + $html .= new \Views\Admin($document); } return $html; diff --git a/core/Documents/Install.class.php b/core/Documents/Install.class.php index 1189317..7873113 100644 --- a/core/Documents/Install.class.php +++ b/core/Documents/Install.class.php @@ -114,6 +114,16 @@ namespace Documents\Install { $step = self::FINISH_INSTALLATION; if(!$config->isFilePresent("JWT") && !$config->create("JWT", generateRandomString(32))) { $this->errorString = "Unable to create jwt file"; + } else { + $req = new \Api\Notifications\Create($user); + $success = $req->execute(array( + "title" => "Welcome", + "message" => "Your Web-base was successfully installed. Check out the admin dashboard. Have fun!", + "groupId" => USER_GROUP_ADMIN) + ); + if (!$success) { + $this->errorString = $req->getLastError(); + } } } @@ -298,6 +308,10 @@ namespace Documents\Install { $success = $sql->insert("User", array("name", "salt", "password")) ->addRow($username, $salt, $hash) + ->returning("uid") + ->execute() + && $sql->insert("UserGroup", array("group_id", "user_id")) + ->addRow(USER_GROUP_ADMIN, $sql->getLastInsertId()) ->execute(); $msg = $sql->getLastError(); @@ -727,14 +741,6 @@ namespace Documents\Install { die(json_encode($response)); } - /*if($this->currentStep == self::CHECKING_REQUIRMENTS) { - $this->getDocument()->getHead()->addJSCode(" - $(document).ready(function() { - retry(); - }); - "); - }*/ - $progressSidebar = $this->createProgressSidebar(); $progressMainview = $this->createProgessMainview(); $errorStyle = ($this->errorString ? '' : ' style="display:none"'); diff --git a/core/Driver/SQL/MySQL.class.php b/core/Driver/SQL/MySQL.class.php index 7b79d1a..47b0f05 100644 --- a/core/Driver/SQL/MySQL.class.php +++ b/core/Driver/SQL/MySQL.class.php @@ -36,6 +36,7 @@ class MySQL extends SQL { return 'mysqli'; } + // Connection Managment public function connect() { if(!is_null($this->connection)) { @@ -164,32 +165,8 @@ class MySQL extends SQL { return ($success && $returnValues) ? $resultRows : $success; } - public function executeCreateTable($createTable) { - $tableName = $createTable->getTableName(); - $ifNotExists = $createTable->ifNotExists() ? " IF NOT EXISTS": ""; - - $entries = array(); - foreach($createTable->getColumns() as $column) { - $entries[] = ($tmp = $this->getColumnDefinition($column)); - if (is_null($tmp)) { - return false; - } - } - - foreach($createTable->getConstraints() as $constraint) { - $entries[] = ($tmp = $this->getConstraintDefinition($constraint)); - if (is_null($tmp)) { - return false; - } - } - - $entries = implode(",", $entries); - $query = "CREATE TABLE$ifNotExists `$tableName` ($entries)"; - return $this->execute($query); - } - public function executeInsert($insert) { - $tableName = $insert->getTableName(); + $tableName = $this->tableName($insert->getTableName()); $columns = $insert->getColumns(); $rows = $insert->getRows(); $onDuplicateKey = $insert->onDuplicateKey() ?? ""; @@ -204,7 +181,7 @@ class MySQL extends SQL { $numColumns = count($rows[0]); } else { $numColumns = count($columns); - $columns = " (`" . implode("`, `", $columns) . "`)"; + $columns = " (" . $this->columnName($columns) . ")"; } $numRows = count($rows); @@ -235,7 +212,7 @@ class MySQL extends SQL { } } - $query = "INSERT INTO `$tableName`$columns VALUES$values$onDuplicateKey"; + $query = "INSERT INTO $tableName$columns VALUES$values$onDuplicateKey"; $success = $this->execute($query, $parameters); if($success) { @@ -247,19 +224,14 @@ class MySQL extends SQL { public function executeSelect($select) { - $columns = array(); - foreach($select->getColumns() as $col) { - $columns[] = $this->columnName($col); - } - - $columns = implode(",", $columns); + $columns = $this->columnName($select->getColumns()); $tables = $select->getTables(); $params = array(); if (is_null($tables) || empty($tables)) { return "SELECT $columns"; } else { - $tables = implode(",", $tables); + $tables = $this->tableName($tables); } $conditions = $select->getConditions(); @@ -275,9 +247,9 @@ class MySQL extends SQL { $joinStr = ""; foreach($joins as $join) { $type = $join->getType(); - $joinTable = $join->getTable(); - $columnA = $join->getColumnA(); - $columnB = $join->getColumnB(); + $joinTable = $this->tableName($join->getTable()); + $columnA = $this->columnName($join->getColumnA()); + $columnB = $this->columnName($join->getColumnB()); $joinStr .= " $type JOIN $joinTable ON $columnA=$columnB"; } } @@ -399,8 +371,10 @@ class MySQL extends SQL { // TODO: check this please.. public function getValueDefinition($value) { - if (is_numeric($value) || is_bool($value)) { + if (is_numeric($value)) { return $value; + } else if(is_bool($value)) { + return $value ? "TRUE" : "FALSE"; } else if(is_null($value)) { return "NULL"; } else if($value instanceof Keyword) { @@ -421,12 +395,22 @@ class MySQL extends SQL { } protected function tableName($table) { - return "`$table`"; + if (is_array($table)) { + $tables = array(); + foreach($table as $t) $tables[] = $this->tableName($t); + return implode(",", $tables); + } else { + return "`$table`"; + } } protected function columnName($col) { if ($col instanceof Keyword) { return $col->getValue(); + } elseif(is_array($col)) { + $columns = array(); + foreach($col as $c) $columns[] = $this->columnName($c); + return implode(",", $columns); } else { if (($index = strrpos($col, ".")) !== FALSE) { $tableName = $this->tableName(substr($col, 0, $index)); @@ -446,12 +430,4 @@ class MySQL extends SQL { return new Keyword("NOW()"); } - public function count($col = NULL) { - if (is_null($col)) { - return new Keyword("COUNT(*) AS count"); - } else { - return new Keyword("COUNT($col) AS count"); - } - } - }; diff --git a/core/Driver/SQL/PostgreSQL.class.php b/core/Driver/SQL/PostgreSQL.class.php index eeabbc2..5df5996 100644 --- a/core/Driver/SQL/PostgreSQL.class.php +++ b/core/Driver/SQL/PostgreSQL.class.php @@ -36,15 +36,6 @@ class PostgreSQL extends SQL { return 'pgsql'; } - public function getLastError() { - $lastError = parent::getLastError(); - if (empty($lastError)) { - $lastError = pg_last_error($this->connection) . " " . pg_last_error($this->connection); - } - - return $lastError; - } - // Connection Managment public function connect() { if(!is_null($this->connection)) { @@ -84,6 +75,15 @@ class PostgreSQL extends SQL { pg_close($this->connection); } + public function getLastError() { + $lastError = parent::getLastError(); + if (empty($lastError)) { + $lastError = pg_last_error($this->connection) . " " . pg_last_error($this->connection); + } + + return $lastError; + } + protected function execute($query, $values = NULL, $returnValues = false) { $this->lastError = ""; @@ -136,30 +136,6 @@ class PostgreSQL extends SQL { } // Querybuilder - public function executeCreateTable($createTable) { - $tableName = $this->tableName($createTable->getTableName()); - $ifNotExists = $createTable->ifNotExists() ? " IF NOT EXISTS": ""; - - $entries = array(); - foreach($createTable->getColumns() as $column) { - $entries[] = ($tmp = $this->getColumnDefinition($column)); - if (is_null($tmp)) { - return false; - } - } - - foreach($createTable->getConstraints() as $constraint) { - $entries[] = ($tmp = $this->getConstraintDefinition($constraint)); - if (is_null($tmp)) { - return false; - } - } - - $entries = implode(",", $entries); - $query = "CREATE TABLE$ifNotExists $tableName ($entries)"; - return $this->execute($query); - } - public function executeInsert($insert) { $tableName = $this->tableName($insert->getTableName()); @@ -408,7 +384,7 @@ class PostgreSQL extends SQL { if ($val instanceof Keyword) { return $val->getValue(); } else { - $params[] = $val; + $params[] = is_bool($val) ? ($val ? "TRUE" : "FALSE") : $val; return '$' . count($params); } } @@ -449,13 +425,5 @@ class PostgreSQL extends SQL { public function currentTimestamp() { return new Keyword("CURRENT_TIMESTAMP"); } - - public function count($col = NULL) { - if (is_null($col)) { - return new Keyword("COUNT(*) AS count"); - } else { - return new Keyword("COUNT(" . $this->columnName($col) . ") AS count"); - } - } } ?> diff --git a/core/Driver/SQL/SQL.class.php b/core/Driver/SQL/SQL.class.php index 247be60..d00bda3 100644 --- a/core/Driver/SQL/SQL.class.php +++ b/core/Driver/SQL/SQL.class.php @@ -63,7 +63,30 @@ abstract class SQL { // TODO: pull code duplicates up // Querybuilder - public abstract function executeCreateTable($query); + public function executeCreateTable($createTable) { + $tableName = $this->tableName($createTable->getTableName()); + $ifNotExists = $createTable->ifNotExists() ? " IF NOT EXISTS": ""; + + $entries = array(); + foreach($createTable->getColumns() as $column) { + $entries[] = ($tmp = $this->getColumnDefinition($column)); + if (is_null($tmp)) { + return false; + } + } + + foreach($createTable->getConstraints() as $constraint) { + $entries[] = ($tmp = $this->getConstraintDefinition($constraint)); + if (is_null($tmp)) { + return false; + } + } + + $entries = implode(",", $entries); + $query = "CREATE TABLE$ifNotExists $tableName ($entries)"; + return $this->execute($query); + } + public abstract function executeInsert($query); public abstract function executeSelect($query); public abstract function executeDelete($query); @@ -79,7 +102,20 @@ abstract class SQL { // Special Keywords and functions public abstract function currentTimestamp(); - public abstract function count($col = NULL); + + public function count($col = NULL) { + if (is_null($col)) { + return new Keyword("COUNT(*) AS count"); + } else { + $col = $this->columnName($col); + return new Keyword("COUNT($col) AS count"); + } + } + + public function distinct($col) { + $col = $this->columnName($col); + return new Keyword("DISTINCT($col)"); + } // Statements protected abstract function execute($query, $values=NULL, $returnValues=false); diff --git a/core/External/JWT.class.php b/core/External/JWT.class.php index ad81484..5a35081 100644 --- a/core/External/JWT.class.php +++ b/core/External/JWT.class.php @@ -26,8 +26,8 @@ class JWT * @param bool $verify Don't skip verification process * * @return object The JWT's payload as a PHP object - * @throws UnexpectedValueException Provided JWT was invalid - * @throws DomainException Algorithm was not provided + * @throws \UnexpectedValueException Provided JWT was invalid + * @throws \DomainException Algorithm was not provided * * @uses jsonDecode * @uses urlsafeB64Decode @@ -36,22 +36,22 @@ class JWT { $tks = explode('.', $jwt); if (count($tks) != 3) { - throw new UnexpectedValueException('Wrong number of segments'); + throw new \UnexpectedValueException('Wrong number of segments'); } list($headb64, $bodyb64, $cryptob64) = $tks; if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))) { - throw new UnexpectedValueException('Invalid segment encoding'); + throw new \UnexpectedValueException('Invalid segment encoding'); } if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64))) { - throw new UnexpectedValueException('Invalid segment encoding'); + throw new \UnexpectedValueException('Invalid segment encoding'); } $sig = JWT::urlsafeB64Decode($cryptob64); if ($verify) { if (empty($header->alg)) { - throw new DomainException('Empty algorithm'); + throw new \DomainException('Empty algorithm'); } if ($sig != JWT::sign("$headb64.$bodyb64", $key, $header->alg)) { - throw new UnexpectedValueException('Signature verification failed'); + throw new \UnexpectedValueException('Signature verification failed'); } } return $payload; @@ -93,7 +93,7 @@ class JWT * algorithms are 'HS256', 'HS384' and 'HS512' * * @return string An encrypted message - * @throws DomainException Unsupported algorithm was specified + * @throws \DomainException Unsupported algorithm was specified */ public static function sign($msg, $key, $method = 'HS256') { @@ -103,7 +103,7 @@ class JWT 'HS512' => 'sha512', ); if (empty($methods[$method])) { - throw new DomainException('Algorithm not supported'); + throw new \DomainException('Algorithm not supported'); } return hash_hmac($methods[$method], $msg, $key, true); } @@ -114,7 +114,7 @@ class JWT * @param string $input JSON string * * @return object Object representation of JSON string - * @throws DomainException Provided string was invalid JSON + * @throws \DomainException Provided string was invalid JSON */ public static function jsonDecode($input) { @@ -122,7 +122,7 @@ class JWT if (function_exists('json_last_error') && $errno = json_last_error()) { JWT::_handleJsonError($errno); } else if ($obj === null && $input !== 'null') { - throw new DomainException('Null result with non-null input'); + throw new \DomainException('Null result with non-null input'); } return $obj; } @@ -133,7 +133,7 @@ class JWT * @param object|array $input A PHP object or array * * @return string JSON representation of the PHP object or array - * @throws DomainException Provided object could not be encoded to valid JSON + * @throws \DomainException Provided object could not be encoded to valid JSON */ public static function jsonEncode($input) { @@ -141,7 +141,7 @@ class JWT if (function_exists('json_last_error') && $errno = json_last_error()) { JWT::_handleJsonError($errno); } else if ($json === 'null' && $input !== null) { - throw new DomainException('Null result with non-null input'); + throw new \DomainException('Null result with non-null input'); } return $json; } @@ -189,7 +189,7 @@ class JWT JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON' ); - throw new DomainException( + throw new \DomainException( isset($messages[$errno]) ? $messages[$errno] : 'Unknown JSON error: ' . $errno diff --git a/core/Objects/User.class.php b/core/Objects/User.class.php index e6c2a34..28de1af 100644 --- a/core/Objects/User.class.php +++ b/core/Objects/User.class.php @@ -2,6 +2,7 @@ namespace Objects; +use \External\JWT; use Driver\SQL\Column\Column; use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\CondBool; @@ -132,8 +133,6 @@ class User extends ApiObject { $this->setLangauge(Language::newInstance($row['langId'], $row['langCode'], $row['langName'])); } } - } else { - var_dump($this->sql->getLastError()); } return $success; @@ -146,7 +145,7 @@ class User extends ApiObject { && ($jwt = $this->configuration->getJWT())) { try { $token = $_COOKIE['session']; - $decoded = (array)\External\JWT::decode($token, $jwt->getKey()); + $decoded = (array)JWT::decode($token, $jwt->getKey()); if(!is_null($decoded)) { $userId = (isset($decoded['userId']) ? $decoded['userId'] : NULL); $sessionId = (isset($decoded['sessionId']) ? $decoded['sessionId'] : NULL); @@ -154,8 +153,8 @@ class User extends ApiObject { $this->readData($userId, $sessionId); } } - } catch(Exception $e) { - echo $e; + } catch(\Exception $e) { + // ignored } } @@ -204,8 +203,6 @@ class User extends ApiObject { $this->setLangauge(Language::newInstance($row['langId'], $row['langCode'], $row['langName'])); } } - } else { - var_dump($this->sql->getLastError()); } return $success; diff --git a/core/Views/Admin.class.php b/core/Views/Admin.class.php new file mode 100644 index 0000000..af91e21 --- /dev/null +++ b/core/Views/Admin.class.php @@ -0,0 +1,47 @@ +createIcon("bars"); + $iconSearch = $this->createIcon("search"); + $iconNotifications = $this->createIcon("bell"); + $header = ""; + + return $header; + } + + private function getMainContent() { + return ""; + } + + private function getSideBar() { + return ""; + } + + public function getCode() { + $html = parent::getCode(); + + $html .= "
"; + $html .= $this->getMainHeader(); + $html .= "
"; + $html .= $this->getSideBar(); + $html .= $this->getMainContent(); + $html .= "
+
"; + + return $html; + } +} + +?> diff --git a/core/Views/Login.class.php b/core/Views/Login.class.php index 3d12b20..bb4bf19 100644 --- a/core/Views/Login.class.php +++ b/core/Views/Login.class.php @@ -49,8 +49,6 @@ class Login extends \View { "; return $html; - - return $html; } } diff --git a/css/admin.css b/css/admin.css index 08c2c09..64713f6 100644 --- a/css/admin.css +++ b/css/admin.css @@ -27,203 +27,27 @@ vertical-align: bottom; } -.device-table > tbody > tr:hover { - cursor: pointer; - background-color: grey; +.main-header { + border-bottom: 1px solid #dee2e6; } -.device-table > tbody > tr > td:nth-child(3) { - text-align: center; +.main-sidebar { + background-color: #343a40; + width: 250px; + position: fixed; + top: 0; + left: 0; + height: 100vh; + z-index: 999; + color: #fff; + transition: all 0.3s; } -.apikey-table > tbody > tr > td:last-child { - float: right; +.content-wrapper { + background: #f4f6f9; } -.apikey-table > tbody > tr > td:first-child { - word-break: break-all; -} - -.apikey-table > tbody > tr:hover { - background-color: grey; - cursor: pointer; -} - -.sidebar { - margin: 0; - padding: 0; - color: white; - text-align: center; -} - -.sidebar a { - color: white; -} - -.status { font-size: 12px; } -.status-ok { color: #38e40d; } -.status-error { color: red; } -.status-offline { color: gray; } - -.sidebar .nav-item { - line-height: 30px; - border-bottom: 1px solid gray; -} - -.nav-link { - grid-template-columns: 20px auto 20px; -} - -.nav-link-center { - grid-column-start: 2; - grid-column-end: 2; -} - -.sidebar-title { - font-size: 18px; - margin-bottom: 0.5em; - font-weight: bold; -} - -.sidebar-top { - height: 100px; - padding: 10px; - border-bottom: 5px solid gray; - background-color: #555; -} - -.sidebar-bottom { - height: 100px; - padding: 10px; - position: absolute; - bottom: 0; - border-top: 5px solid gray; +.main-wrapper { + display: flex; width: 100%; - grid-template-rows: 50% 50%; -} - -.grid { - display: grid; -} - -.grid > span { - align-self: center; -} - -.sidebar-bottom > a { - grid-template-columns: 20px auto; - align-self: center; -} - -.sidebar-bottom > a:hover { - text-decoration: none; - font-weight: bold; -} - -.sidebar .active .nav-link-center { - text-decoration: underline; - font-weight: bold; -} - -.sidebar .nav-item:hover { - background-color: gray; -} - -.nav-device { - background-color: #5a5a5a; -} - -.service { - margin: 10px; -} - -.service > .card-header { - color: black; - cursor: pointer; - display: grid; - grid-template-columns: 20px auto; -} - -.service-icon { - width: 32px; - height: 32px; -} - -.fs-listview > tbody > tr:hover { - cursor: pointer; -} - -.fs-listview > tbody > tr.downloading > td, .fs-gridview > div.downloading > span { - font-style: italic; - color: gray; -} - -.fs-toolbar { - display: grid; - grid-template-columns: 1px 40px 40px 1px auto 1px 40px 1px 40px 40px 1px; -} - -.fs-toolbar > i { - align-self: center; - cursor: pointer; -} - -.fs-toolbar > span { - text-align: left; - align-self: center; -} - -.fs-gridview { - display: grid; - grid-template-columns: repeat(4, auto); -} - -.fs-gridview div { - align-self: center; - text-align: center; -} - -.fs-gridview > div { - padding: 5px; - display: grid; - grid-template-rows: 48px auto; -} - -.fs-gridview > div:hover { - background: #ddd; - cursor: pointer; -} - -.vr { - border-left: 1px solid #dee2e6; - height: 100%; -} - -.camera-stream { - cursor: pointer; -} - -.temperature-controls { - display: grid; - grid-template-columns: auto 30px; -} - -.temperature-controls > div { - align-self: center; - text-align: left; -} - -.shell-tabs > li > a { - color: black; - line-height: inherit; - outline: none; -} - -.shell { - text-align: left -} - -.speaker-controls { - display:grid; - grid-template-columns: auto auto auto; } diff --git a/index.php b/index.php index 398bb0d..aced544 100644 --- a/index.php +++ b/index.php @@ -44,9 +44,9 @@ if(isset($_GET["api"]) && is_string($_GET["api"])) { header("400 Bad Request"); $response = createError("Invalid Method"); } else { - $apiFunction = strtoupper($apiFunction[0]) . substr($apiFunction, 1); - $apiFunction = str_replace("/", "\\", $apiFunction); - $class = "\\Api\\$apiFunction"; + $apiFunction = implode("\\", array_map('ucfirst', explode("/", $apiFunction))); + if($apiFunction[0] !== "\\") $apiFunction = "\\$apiFunction"; + $class = "\\Api$apiFunction"; $file = getClassPath($class); if(!file_exists($file)) { header("404 Not Found"); @@ -63,10 +63,15 @@ if(isset($_GET["api"]) && is_string($_GET["api"])) { } } } else { + $documentName = $_GET["site"]; if ($installation) { - $document = new Documents\Install($user); + if ($documentName !== "" && $documentName !== "index.php") { + $response = "Redirecting to /"; + header("Location: /"); + } else { + $document = new Documents\Install($user); + } } else { - $documentName = $_GET["site"]; if(empty($documentName) || strcasecmp($documentName, "install") === 0) { $documentName = "home"; } else if(!preg_match("/[a-zA-Z]+(\/[a-zA-Z]+)*/", $documentName)) { diff --git a/js/admin.js b/js/admin.js index 4cb2ff0..48e339d 100644 --- a/js/admin.js +++ b/js/admin.js @@ -11,7 +11,7 @@ $(document).ready(function() { errorDiv.hide(); btn.prop("disabled", true); btn.html("Logging in… "); - jsCore.apiCall("login", {"username": username, "password": password}, function(data) { + jsCore.apiCall("user/login", {"username": username, "password": password}, function(data) { window.location.reload(); }, function(err) { btn.html("Login"); diff --git a/js/install.js b/js/install.js index ec661ec..d980596 100644 --- a/js/install.js +++ b/js/install.js @@ -123,7 +123,7 @@ $(document).ready(function() { }); $("#btnFinish").click(function() { - window.location.reload(); + window.location = "/admin"; }); $("#btnRetry").click(function() { diff --git a/test/apiTest.py b/test/apiTest.py index b9ef596..fc324a0 100644 --- a/test/apiTest.py +++ b/test/apiTest.py @@ -6,10 +6,16 @@ class ApiTestCase(PhpTest): super().__init__({ "Testing login…": self.test_login, "Testing already logged in…": self.test_already_logged_in, + + # 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 revoke api key…": self.test_revoke_api_key, + + # Notifications + "Testing fetch notifications…": self.test_fetch_notifications, + "Testing logout…": self.test_logout, }) @@ -17,12 +23,12 @@ class ApiTestCase(PhpTest): return "/api/%s" % method def getApiKeys(self): - obj = self.httpPost(self.api("getApiKeys")) + obj = self.httpPost(self.api("apiKey/fetch")) self.assertEquals(True, obj["success"], obj["msg"]) return obj def test_login(self): - obj = self.httpPost(self.api("login"), data={ "username": PhpTest.ADMIN_USERNAME, "password": PhpTest.ADMIN_PASSWORD }) + obj = self.httpPost(self.api("user/login"), data={ "username": PhpTest.ADMIN_USERNAME, "password": PhpTest.ADMIN_PASSWORD }) self.assertEquals(True, obj["success"], obj["msg"]) return obj @@ -35,7 +41,7 @@ class ApiTestCase(PhpTest): self.assertEquals([], obj["api_keys"]) def test_create_api_key(self): - obj = self.httpPost(self.api("createApiKey")) + obj = self.httpPost(self.api("apiKey/create")) self.assertEquals(True, obj["success"], obj["msg"]) self.assertTrue("api_key" in obj) self.apiKey = obj["api_key"] @@ -45,18 +51,22 @@ class ApiTestCase(PhpTest): self.assertDictEqual(self.apiKey, obj["api_keys"][0]) def test_refresh_api_key(self): - obj = self.httpPost(self.api("refreshApiKey"), data={"id": self.apiKey["uid"]}) + obj = self.httpPost(self.api("apiKey/refresh"), data={"id": self.apiKey["uid"]}) self.assertEquals(True, obj["success"], obj["msg"]) self.assertTrue("valid_until" in obj) self.assertTrue(obj["valid_until"] >= self.apiKey["valid_until"]) def test_revoke_api_key(self): - obj = self.httpPost(self.api("revokeApiKey"), data={"id": self.apiKey["uid"]}) + obj = self.httpPost(self.api("apiKey/revoke"), data={"id": self.apiKey["uid"]}) self.assertEquals(True, obj["success"], obj["msg"]) self.test_get_api_keys_empty() - def test_logout(self): - obj = self.httpPost(self.api("logout")) + def test_fetch_notifications(self): + obj = self.httpPost(self.api("notifications/fetch")) self.assertEquals(True, obj["success"], obj["msg"]) - obj = self.httpPost(self.api("logout")) + + def test_logout(self): + obj = self.httpPost(self.api("user/logout")) + self.assertEquals(True, obj["success"], obj["msg"]) + obj = self.httpPost(self.api("user/logout")) self.assertEquals(False, obj["success"])