diff --git a/Documentation/database_mld.puml b/Documentation/database_mld.puml index 170fad7..8b4d32f 100644 --- a/Documentation/database_mld.puml +++ b/Documentation/database_mld.puml @@ -5,8 +5,8 @@ object Account { name age email - phoneNumber - passwordHash + phone_number + password_hash profile_picture } diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 1a756f6..77f2b3d 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -4,6 +4,12 @@ DROP TABLE IF EXISTS Tactic; DROP TABLE IF EXISTS Team; DROP TABLE IF EXISTS User; DROP TABLE IF EXISTS Member; + +CREATE TABLE Admins +( + id integer PRIMARY KEY REFERENCES Account +); + CREATE TABLE Account ( id integer PRIMARY KEY AUTOINCREMENT, @@ -11,8 +17,7 @@ CREATE TABLE Account username varchar NOT NULL, token varchar UNIQUE NOT NULL, hash varchar NOT NULL, - profile_picture varchar NOT NULL, - is_admin boolean DEFAULT false NOT NULL + profile_picture varchar NOT NULL ); CREATE TABLE Tactic diff --git a/src/Api/APIControl.php b/src/Api/APIControl.php new file mode 100644 index 0000000..260575a --- /dev/null +++ b/src/Api/APIControl.php @@ -0,0 +1,44 @@ + $schema an array of `fieldName => DefaultValidators` which represents the request object schema + * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. + * The callback must accept an HttpRequest, and return an HttpResponse object. + * @return HttpResponse + */ + public static function runChecked(array $schema, callable $run): HttpResponse { + return Control::runChecked($schema, $run, self::errorFactory()); + } + + /** + * Runs given callback, if the given request data array validates the given schema. + * @param array $data the request's data array. + * @param array $schema an array of `fieldName => DefaultValidators` which represents the request object schema + * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. + * The callback must accept an HttpRequest, and return an HttpResponse object. + * @return HttpResponse + */ + public static function runCheckedFrom(array $data, array $schema, callable $run): HttpResponse { + return Control::runCheckedFrom($data, $schema, $run, self::errorFactory()); + } + +} diff --git a/src/Api/Controller/APIAccountsController.php b/src/Api/Controller/APIAccountsController.php index 1956b80..13d9db4 100644 --- a/src/Api/Controller/APIAccountsController.php +++ b/src/Api/Controller/APIAccountsController.php @@ -2,6 +2,7 @@ namespace IQBall\Api\Controller; +use IQBall\Api\APIControl; use IQBall\App\Control; use IQBall\Core\Data\Account; use IQBall\Core\Gateway\AccountGateway; @@ -9,8 +10,8 @@ use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; -use IQBall\Core\Validation\DefaultValidators; use IQBall\Core\Model\AuthModel; +use IQBall\Core\Validation\DefaultValidators; use IQBall\Core\Validation\ValidationFail; class APIAccountsController { @@ -18,6 +19,7 @@ class APIAccountsController { private AuthModel $authModel; /** + * @param AuthModel $model * @param AccountGateway $accounts */ public function __construct(AuthModel $model, AccountGateway $accounts) { @@ -31,7 +33,7 @@ class APIAccountsController { * @return HttpResponse */ public function listUsers(array $request): HttpResponse { - return Control::runCheckedFrom($request, [ + return APIControl::runCheckedFrom($request, [ 'start' => [DefaultValidators::isUnsignedInteger()], 'n' => [DefaultValidators::isUnsignedInteger()], 'search' => [DefaultValidators::lenBetween(0, 256)], @@ -42,7 +44,7 @@ class APIAccountsController { "users" => $users, "totalCount" => $this->accounts->totalCount(), ]); - }, true); + }); } /** @@ -60,7 +62,7 @@ class APIAccountsController { } public function addUser(): HttpResponse { - return Control::runChecked([ + return APIControl::runChecked([ "username" => [DefaultValidators::name()], "email" => [DefaultValidators::email()], "password" => [DefaultValidators::password()], @@ -76,20 +78,20 @@ class APIAccountsController { return new JsonHttpResponse([ "id" => $account->getUser()->getId(), ]); - }, true); + }); } public function removeUsers(): HttpResponse { - return Control::runChecked([ + return APIControl::runChecked([ "identifiers" => [DefaultValidators::array(), DefaultValidators::forall(DefaultValidators::isUnsignedInteger())], ], function (HttpRequest $req) { $this->accounts->removeAccounts($req["identifiers"]); return HttpResponse::fromCode(HttpCodes::OK); - }, true); + }); } public function updateUser(int $id): HttpResponse { - return Control::runChecked([ + return APIControl::runChecked([ "email" => [DefaultValidators::email()], "username" => [DefaultValidators::name()], "isAdmin" => [DefaultValidators::bool()], @@ -101,6 +103,6 @@ class APIAccountsController { $this->authModel->update($id, $req["email"], $req["username"], $req["isAdmin"]); return HttpResponse::fromCode(HttpCodes::OK); - }, true); + }); } } diff --git a/src/Api/Controller/APIAuthController.php b/src/Api/Controller/APIAuthController.php index d21fea3..c715803 100644 --- a/src/Api/Controller/APIAuthController.php +++ b/src/Api/Controller/APIAuthController.php @@ -2,6 +2,7 @@ namespace IQBall\Api\Controller; +use IQBall\Api\APIControl; use IQBall\App\Control; use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpRequest; @@ -26,7 +27,7 @@ class APIAuthController { * @return HttpResponse */ public function authorize(): HttpResponse { - return Control::runChecked([ + return APIControl::runChecked([ "email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)], "password" => [DefaultValidators::password()], ], function (HttpRequest $req) { @@ -38,6 +39,6 @@ class APIAuthController { } return new JsonHttpResponse(["authorization" => $account->getToken()]); - }, true); + }); } } diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 0d99f41..9f71212 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -2,7 +2,7 @@ namespace IQBall\Api\Controller; -use IQBall\App\Control; +use IQBall\Api\APIControl; use IQBall\Core\Data\Account; use IQBall\Core\Data\TacticInfo; use IQBall\Core\Http\HttpCodes; @@ -32,7 +32,7 @@ class APITacticController { * @return HttpResponse */ public function updateName(int $tactic_id, Account $account): HttpResponse { - return Control::runChecked([ + return APIControl::runChecked([ "name" => [DefaultValidators::lenBetween(1, 50), DefaultValidators::nameWithSpaces()], ], function (HttpRequest $request) use ($tactic_id, $account) { @@ -44,23 +44,23 @@ class APITacticController { } return HttpResponse::fromCode(HttpCodes::OK); - }, true); + }); } /** * @param int $id - * @param Account $account * @return HttpResponse */ public function saveContent(int $id, Account $account): HttpResponse { - return Control::runChecked([ + return APIControl::runChecked([ "content" => [], ], function (HttpRequest $req) use ($id) { + //TODO verify that the account has the rights to update the tactic content if ($fail = $this->model->updateContent($id, json_encode($req["content"]))) { return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST); } return HttpResponse::fromCode(HttpCodes::OK); - }, true); + }); } diff --git a/src/App/App.php b/src/App/App.php index cba59de..5f208bc 100644 --- a/src/App/App.php +++ b/src/App/App.php @@ -90,7 +90,6 @@ class App { if ($action->getAuthType() == Action::AUTH_ADMIN && !$account->getUser()->isAdmin()) { return new JsonHttpResponse([ValidationFail::unauthorized()], HttpCodes::UNAUTHORIZED); } - } return $action->run($params, $session); diff --git a/src/App/AppControl.php b/src/App/AppControl.php new file mode 100644 index 0000000..c313e69 --- /dev/null +++ b/src/App/AppControl.php @@ -0,0 +1,44 @@ + $failures], HttpCodes::BAD_REQUEST); + } + }; + } + + /** + * Runs given callback, if the request's payload json validates the given schema. + * @param array $schema an array of `fieldName => DefaultValidators` which represents the request object schema + * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. + * The callback must accept an HttpRequest, and return an HttpResponse object. + * @return HttpResponse + */ + public static function runChecked(array $schema, callable $run): HttpResponse { + return Control::runChecked($schema, $run, self::errorFactory()); + } + + /** + * Runs given callback, if the given request data array validates the given schema. + * @param array $data the request's data array. + * @param array $schema an array of `fieldName => DefaultValidators` which represents the request object schema + * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. + * The callback must accept an HttpRequest, and return an HttpResponse object. + * @return HttpResponse + */ + public static function runCheckedFrom(array $data, array $schema, callable $run): HttpResponse { + return Control::runCheckedFrom($data, $schema, $run, self::errorFactory()); + } + +} diff --git a/src/App/Controller/UserController.php b/src/App/Controller/UserController.php index 6f56128..616bf54 100644 --- a/src/App/Controller/UserController.php +++ b/src/App/Controller/UserController.php @@ -5,6 +5,7 @@ namespace IQBall\App\Controller; use IQBall\App\Session\MutableSessionHandle; use IQBall\App\Session\SessionHandle; use IQBall\App\ViewHttpResponse; +use IQBall\Core\Gateway\AccountGateway; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TeamModel; diff --git a/src/App/Control.php b/src/Core/Control.php similarity index 67% rename from src/App/Control.php rename to src/Core/Control.php index 7f04cbe..106052d 100644 --- a/src/App/Control.php +++ b/src/Core/Control.php @@ -1,6 +1,6 @@ $schema an array of `fieldName => DefaultValidators` which represents the request object schema * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. * The callback must accept an HttpRequest, and return an HttpResponse object. - * @param bool $errorInJson if set to true, the returned response will be a JsonHttpResponse if the schema isn't validated + * @param ControlSchemaErrorResponseFactory $errorFactory an error factory to use if the request does not validate the required schema * @return HttpResponse */ - public static function runChecked(array $schema, callable $run, bool $errorInJson = false): HttpResponse { + public static function runChecked(array $schema, callable $run, ControlSchemaErrorResponseFactory $errorFactory): HttpResponse { $request_body = file_get_contents('php://input'); $payload_obj = json_decode($request_body); if (!$payload_obj instanceof \stdClass) { $fail = new ValidationFail("bad-payload", "request body is not a valid json object"); - - if ($errorInJson) { - return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST); - } - - return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST); + return $errorFactory->apply([$fail]); } $payload = get_object_vars($payload_obj); - return self::runCheckedFrom($payload, $schema, $run, $errorInJson); + return self::runCheckedFrom($payload, $schema, $run, $errorFactory); } /** @@ -40,18 +35,15 @@ class Control { * @param array $schema an array of `fieldName => DefaultValidators` which represents the request object schema * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. * The callback must accept an HttpRequest, and return an HttpResponse object. - * @param bool $errorInJson if set to true, the returned response will be a JsonHttpResponse if the schema isn't validated + * @param ControlSchemaErrorResponseFactory $errorFactory an error factory to use if the request does not validate the required schema * @return HttpResponse */ - public static function runCheckedFrom(array $data, array $schema, callable $run, bool $errorInJson = false): HttpResponse { + public static function runCheckedFrom(array $data, array $schema, callable $run, ControlSchemaErrorResponseFactory $errorFactory): HttpResponse { $fails = []; $request = HttpRequest::from($data, $fails, $schema); if (!empty($fails)) { - if ($errorInJson) { - return new JsonHttpResponse($fails, HttpCodes::BAD_REQUEST); - } - return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST); + return $errorFactory->apply($fails); } return call_user_func_array($run, [$request]); diff --git a/src/Core/ControlSchemaErrorResponseFactory.php b/src/Core/ControlSchemaErrorResponseFactory.php new file mode 100644 index 0000000..9882a65 --- /dev/null +++ b/src/Core/ControlSchemaErrorResponseFactory.php @@ -0,0 +1,14 @@ +con->exec("UPDATE Account SET username = :username, email = :email, token = :token, is_admin = :is_admin WHERE id = :id", [ + $this->con->exec("UPDATE Account SET username = :username, email = :email, token = :token WHERE id = :id", [ ':username' => [$name, PDO::PARAM_STR], ':email' => [$email, PDO::PARAM_STR], ':token' => [$token, PDO::PARAM_STR], ':id' => [$id, PDO::PARAM_INT], - ':is_admin' => [$isAdmin, PDO::PARAM_BOOL], ]); + $this->setIsAdmin($id, $isAdmin); + } + + public function isAdmin(int $id): bool { + $stmnt = $this->con->prepare("SELECT * FROM Admins WHERE id = :id"); + $stmnt->bindValue(':id', $id, PDO::PARAM_INT); + $stmnt->execute(); + $result = $stmnt->fetchAll(PDO::FETCH_ASSOC); + + return !empty($result); } @@ -46,8 +55,11 @@ class AccountGateway { * @return bool true if the given user exists */ public function setIsAdmin(int $id, bool $isAdmin): bool { - $stmnt = $this->con->prepare("UPDATE Account SET is_admin = :is_admin WHERE id = :id"); - $stmnt->bindValue(':is_admin', $isAdmin); + if ($isAdmin) { + $stmnt = $this->con->prepare("INSERT INTO Admins VALUES(:id)"); + } else { + $stmnt = $this->con->prepare("DELETE FROM Admins WHERE id = :id"); + } $stmnt->bindValue(':id', $id); $stmnt->execute(); @@ -92,7 +104,7 @@ class AccountGateway { return null; } - return new Account($acc["token"], new User($email, $acc["username"], $acc["id"], $acc["profile_picture"], $acc['is_admin'])); + return new Account($acc["token"], new User($email, $acc["username"], $acc["id"], $acc["profile_picture"], $this->isAdmin($acc["id"]))); } /** @@ -123,7 +135,7 @@ class AccountGateway { return null; } - return new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $acc["is_admin"])); + return new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $this->isAdmin($acc["id"]))); } /** @@ -142,7 +154,7 @@ class AccountGateway { ":search" => [$searchString ?? "", PDO::PARAM_STR], ] ); - return array_map(fn(array $acc) => new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $acc["is_admin"])), $res); + return array_map(fn(array $acc) => new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $this->isAdmin($acc["id"]))), $res); } /**