From 9cde3af510d0f95819cf10373d72402502a6065d Mon Sep 17 00:00:00 2001 From: Override-6 Date: Fri, 8 Dec 2023 17:51:25 +0100 Subject: [PATCH 1/6] add basic api routes to get info on server, users and tactics --- Documentation/database_mld.puml | 2 +- public/api/index.php | 22 ++++++- sql/database.php | 18 ++++++ sql/setup-tables.sql | 15 ++--- src/Api/API.php | 20 ++++--- src/Api/Controller/APIAccountsController.php | 55 +++++++++++++++++ src/Api/Controller/APIAuthController.php | 3 +- src/Api/Controller/APIServerController.php | 45 ++++++++++++++ src/Api/Controller/APITacticController.php | 26 +++++++- src/App/App.php | 9 ++- src/App/Control.php | 21 +++++-- src/Core/Action.php | 26 +++++--- src/Core/Data/User.php | 18 +++++- src/Core/Gateway/AccountGateway.php | 62 ++++++++++++++++++-- src/Core/Gateway/MemberGateway.php | 4 +- src/Core/Gateway/TacticInfoGateway.php | 18 ++++++ src/Core/Model/AuthModel.php | 12 +++- src/Core/Model/TacticModel.php | 12 ++++ src/Core/Validation/DefaultValidators.php | 6 +- 19 files changed, 342 insertions(+), 52 deletions(-) create mode 100644 src/Api/Controller/APIAccountsController.php create mode 100644 src/Api/Controller/APIServerController.php diff --git a/Documentation/database_mld.puml b/Documentation/database_mld.puml index 2c33ce2..170fad7 100644 --- a/Documentation/database_mld.puml +++ b/Documentation/database_mld.puml @@ -7,7 +7,7 @@ object Account { email phoneNumber passwordHash - profilePicture + profile_picture } object TacticFolder { diff --git a/public/api/index.php b/public/api/index.php index da25013..91fa23d 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -6,7 +6,9 @@ require "../../sql/database.php"; require "../../src/index-utils.php"; use IQBall\Api\API; +use IQBall\Api\Controller\APIAccountsController; use IQBall\Api\Controller\APIAuthController; +use IQBall\Api\Controller\APIServerController; use IQBall\Api\Controller\APITacticController; use IQBall\App\Session\PhpSessionHandle; use IQBall\Core\Action; @@ -17,6 +19,8 @@ use IQBall\Core\Gateway\TacticInfoGateway; use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\TacticModel; +$basePath = get_public_path(__DIR__); + function getTacticController(): APITacticController { return new APITacticController(new TacticModel(new TacticInfoGateway(new Connection(get_database())))); } @@ -25,14 +29,30 @@ function getAuthController(): APIAuthController { return new APIAuthController(new AuthModel(new AccountGateway(new Connection(get_database())))); } +function getAccountController(): APIAccountsController { + $con = new Connection(get_database()); + return new APIAccountsController(new AccountGateway($con)); +} + +function getServerController(): APIServerController { + global $basePath; + return new APIServerController($basePath, get_database()); +} + function getRoutes(): AltoRouter { $router = new AltoRouter(); - $router->setBasePath(get_public_path(__DIR__)); + global $basePath; + $router->setBasePath($basePath); $router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize())); $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); $router->map("POST", "/tactic/[i:id]/save", Action::auth(fn(int $id, Account $acc) => getTacticController()->saveContent($id, $acc))); + $router->map("GET", "/admin/list-users", Action::admin(fn() => getAccountController()->listUsers($_GET))); + $router->map("GET", "/admin/user/[i:id]", Action::admin(fn(int $id) => getAccountController()->getUser($id))); + $router->map("GET", "/admin/user/[i:id]/space", Action::admin(fn(int $id) => getTacticController()->getUserTactics($id))); + $router->map("GET", "/admin/server-info", Action::admin(fn() => getServerController()->getServerInfo())); + return $router; } diff --git a/sql/database.php b/sql/database.php index 8f5aa9d..c301fda 100644 --- a/sql/database.php +++ b/sql/database.php @@ -1,5 +1,9 @@ insertAccount($name, $email, AuthModel::generateToken(), password_hash("123456", PASSWORD_DEFAULT)); + $accounts->setIsAdmin($id, true); + } +} diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 0d157d9..1a756f6 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -11,7 +11,8 @@ CREATE TABLE Account username varchar NOT NULL, token varchar UNIQUE NOT NULL, hash varchar NOT NULL, - profilePicture varchar NOT NULL + profile_picture varchar NOT NULL, + is_admin boolean DEFAULT false NOT NULL ); CREATE TABLE Tactic @@ -25,13 +26,6 @@ CREATE TABLE Tactic FOREIGN KEY (owner) REFERENCES Account ); -CREATE TABLE FormEntries -( - name varchar NOT NULL, - description varchar NOT NULL -); - - CREATE TABLE Team ( id integer PRIMARY KEY AUTOINCREMENT NOT NULL, @@ -41,11 +35,10 @@ CREATE TABLE Team second_color varchar NOT NULL ); - CREATE TABLE Member ( - id_team integer NOT NULL, - id_user integer NOT NULL, + id_team integer NOT NULL, + id_user integer NOT NULL, role text CHECK (role IN ('COACH', 'PLAYER')) NOT NULL, FOREIGN KEY (id_team) REFERENCES Team (id), FOREIGN KEY (id_user) REFERENCES Account (id) diff --git a/src/Api/API.php b/src/Api/API.php index 080db59..22b9566 100644 --- a/src/Api/API.php +++ b/src/Api/API.php @@ -4,6 +4,8 @@ namespace IQBall\Api; use Exception; use IQBall\Core\Action; +use IQBall\Core\Data\Account; +use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Validation\ValidationFail; @@ -27,7 +29,7 @@ class API { /** * @param array|false $match - * @param callable $tryGetAuthorization function to return an authorisation object for the given action (if required) + * @param callable(): Account $tryGetAuthorization function to return account authorisation for the given action (if required) * @return HttpResponse * @throws Exception */ @@ -41,15 +43,19 @@ class API { throw new Exception("routed action is not an AppAction object."); } - $auth = null; + $account = null; - if ($action->isAuthRequired()) { - $auth = call_user_func($tryGetAuthorization); - if ($auth == null) { - return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")]); + if ($action->getAuthType() != Action::NO_AUTH) { + $account = call_user_func($tryGetAuthorization); + if ($account == null) { + return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")], HttpCodes::UNAUTHORIZED); + } + + if ($action->getAuthType() == Action::AUTH_ADMIN && !$account->getUser()->isAdmin()) { + return new JsonHttpResponse([ValidationFail::unauthorized()], HttpCodes::UNAUTHORIZED); } } - return $action->run($match['params'], $auth); + return $action->run($match['params'], $account); } } diff --git a/src/Api/Controller/APIAccountsController.php b/src/Api/Controller/APIAccountsController.php new file mode 100644 index 0000000..3fe2de0 --- /dev/null +++ b/src/Api/Controller/APIAccountsController.php @@ -0,0 +1,55 @@ +accounts = $accounts; + } + + + /** + * @param array $request + * @return HttpResponse + */ + public function listUsers(array $request): HttpResponse { + return Control::runCheckedFrom($request, [ + 'start' => [DefaultValidators::isUnsignedInteger()], + 'n' => [DefaultValidators::isUnsignedInteger()], + ], function (HttpRequest $req) { + $accounts = $this->accounts->listAccounts(intval($req['start']), intval($req['n'])); + $response = array_map(fn(Account $acc) => $acc->getUser(), $accounts); + return new JsonHttpResponse($response); + }, true); + } + + + /** + * @param int $userId + * @return HttpResponse given user information. + */ + public function getUser(int $userId): HttpResponse { + $acc = $this->accounts->getAccount($userId); + + if ($acc == null) { + return new JsonHttpResponse([ValidationFail::notFound("User not found")], HttpCodes::NOT_FOUND); + } + + return new JsonHttpResponse($acc->getUser()); + } +} diff --git a/src/Api/Controller/APIAuthController.php b/src/Api/Controller/APIAuthController.php index 8e6291c..e257844 100644 --- a/src/Api/Controller/APIAuthController.php +++ b/src/Api/Controller/APIAuthController.php @@ -38,7 +38,6 @@ class APIAuthController { } return new JsonHttpResponse(["authorization" => $account->getToken()]); - }); + }, true); } - } diff --git a/src/Api/Controller/APIServerController.php b/src/Api/Controller/APIServerController.php new file mode 100644 index 0000000..1c82d3e --- /dev/null +++ b/src/Api/Controller/APIServerController.php @@ -0,0 +1,45 @@ +basePath = $basePath; + $this->pdo = $pdo; + } + + private function countLines(string $table): int { + $stmnt = $this->pdo->prepare("SELECT count(*) FROM $table"); + $stmnt->execute(); + $res = $stmnt->fetch(\PDO::FETCH_BOTH); + return $res[0]; + } + + + /** + * @return HttpResponse some (useless) information about the server + */ + public function getServerInfo(): HttpResponse { + + return new JsonHttpResponse([ + 'base_path' => $this->basePath, + 'date' => (int) gettimeofday(true) * 1000, + 'database' => [ + 'accounts' => $this->countLines("Account") . " line(s)", + 'tactics' => $this->countLines("Tactic") . " line(s)", + 'teams' => $this->countLines("Team") . " line(s)", + ], + ]); + } + +} diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 2156538..0d99f41 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -4,12 +4,12 @@ namespace IQBall\Api\Controller; use IQBall\App\Control; use IQBall\Core\Data\Account; +use IQBall\Core\Data\TacticInfo; use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Model\TacticModel; -use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\DefaultValidators; /** @@ -44,7 +44,7 @@ class APITacticController { } return HttpResponse::fromCode(HttpCodes::OK); - }); + }, true); } /** @@ -60,6 +60,26 @@ class APITacticController { return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST); } return HttpResponse::fromCode(HttpCodes::OK); - }); + }, true); } + + + /** + * @param int $userId + * @return HttpResponse given user information. + */ + public function getUserTactics(int $userId): HttpResponse { + $tactics = $this->model->listAllOf($userId); + + $response = array_map(fn(TacticInfo $t) => [ + 'id' => $t->getId(), + 'name' => $t->getName(), + 'court' => $t->getCourtType(), + 'creation_date' => $t->getCreationDate(), + ], $tactics); + + return new JsonHttpResponse($response); + } + + } diff --git a/src/App/App.php b/src/App/App.php index cd3c293..cba59de 100644 --- a/src/App/App.php +++ b/src/App/App.php @@ -4,8 +4,10 @@ namespace IQBall\App; use IQBall\App\Session\MutableSessionHandle; use IQBall\Core\Action; +use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; +use IQBall\Core\Validation\ValidationFail; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; @@ -77,13 +79,18 @@ class App { * @return HttpResponse */ public static function runAction(string $authRoute, Action $action, array $params, MutableSessionHandle $session): HttpResponse { - if ($action->isAuthRequired()) { + if ($action->getAuthType() != Action::NO_AUTH) { $account = $session->getAccount(); if ($account == null) { // put in the session the initial url the user wanted to get $session->setInitialTarget($_SERVER['REQUEST_URI']); return HttpResponse::redirect($authRoute); } + + 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/Control.php b/src/App/Control.php index b8148bb..0e517e4 100644 --- a/src/App/Control.php +++ b/src/App/Control.php @@ -5,6 +5,7 @@ namespace IQBall\App; use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; +use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\Validator; @@ -13,18 +14,24 @@ class Control { * Runs given callback, if the request's 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. + * 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 * @return HttpResponse */ - public static function runChecked(array $schema, callable $run): HttpResponse { + public static function runChecked(array $schema, callable $run, bool $errorInJson = false): 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]); + } + return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST); } $payload = get_object_vars($payload_obj); - return self::runCheckedFrom($payload, $schema, $run); + return self::runCheckedFrom($payload, $schema, $run, $errorInJson); } /** @@ -32,14 +39,18 @@ class Control { * @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. + * 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 * @return HttpResponse */ - public static function runCheckedFrom(array $data, array $schema, callable $run): HttpResponse { + public static function runCheckedFrom(array $data, array $schema, callable $run, bool $errorInJson = false): HttpResponse { $fails = []; $request = HttpRequest::from($data, $fails, $schema); if (!empty($fails)) { + if ($errorInJson) { + return new JsonHttpResponse($fails); + } return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST); } diff --git a/src/Core/Action.php b/src/Core/Action.php index 35721c1..df40ea9 100644 --- a/src/Core/Action.php +++ b/src/Core/Action.php @@ -9,23 +9,27 @@ use IQBall\Core\Http\HttpResponse; * @template S session */ class Action { + public const NO_AUTH = 1; + public const AUTH_USER = 2; + public const AUTH_ADMIN = 3; + /** * @var callable(mixed[], S): HttpResponse $action action to call */ protected $action; - private bool $isAuthRequired; + private int $authType; /** * @param callable(mixed[], S): HttpResponse $action */ - protected function __construct(callable $action, bool $isAuthRequired) { + protected function __construct(callable $action, int $authType) { $this->action = $action; - $this->isAuthRequired = $isAuthRequired; + $this->authType = $authType; } - public function isAuthRequired(): bool { - return $this->isAuthRequired; + public function getAuthType(): int { + return $this->authType; } /** @@ -45,7 +49,7 @@ class Action { * @return Action an action that does not require to have an authorization. */ public static function noAuth(callable $action): Action { - return new Action($action, false); + return new Action($action, self::NO_AUTH); } /** @@ -53,6 +57,14 @@ class Action { * @return Action an action that does require to have an authorization. */ public static function auth(callable $action): Action { - return new Action($action, true); + return new Action($action, self::AUTH_USER); + } + + /** + * @param callable(mixed[], S): HttpResponse $action + * @return Action an action that does require to have an authorization, and to be an administrator. + */ + public static function admin(callable $action): Action { + return new Action($action, self::AUTH_ADMIN); } } diff --git a/src/Core/Data/User.php b/src/Core/Data/User.php index 71e0dd1..02a44c0 100644 --- a/src/Core/Data/User.php +++ b/src/Core/Data/User.php @@ -2,8 +2,6 @@ namespace IQBall\Core\Data; -use _PHPStan_4c4f22f13\Nette\Utils\Json; - class User implements \JsonSerializable { /** * @var string $email user's mail address @@ -25,17 +23,31 @@ class User implements \JsonSerializable { */ private string $profilePicture; + /** + * @var bool isAdmin + */ + private bool $isAdmin; + /** * @param string $email * @param string $name * @param int $id * @param string $profilePicture + * @param bool $isAdmin */ - public function __construct(string $email, string $name, int $id, string $profilePicture) { + public function __construct(string $email, string $name, int $id, string $profilePicture, bool $isAdmin) { $this->email = $email; $this->name = $name; $this->id = $id; $this->profilePicture = $profilePicture; + $this->isAdmin = $isAdmin; + } + + /** + * @return bool + */ + public function isAdmin(): bool { + return $this->isAdmin; } /** diff --git a/src/Core/Gateway/AccountGateway.php b/src/Core/Gateway/AccountGateway.php index a9c3e18..6251b85 100644 --- a/src/Core/Gateway/AccountGateway.php +++ b/src/Core/Gateway/AccountGateway.php @@ -2,6 +2,7 @@ namespace IQBall\Core\Gateway; +use Cassandra\PreparedStatement; use IQBall\Core\Connection; use IQBall\Core\Data\Account; use IQBall\Core\Data\User; @@ -18,7 +19,7 @@ class AccountGateway { } public function insertAccount(string $name, string $email, string $token, string $hash, string $profilePicture): int { - $this->con->exec("INSERT INTO Account(username, hash, email, token,profilePicture) VALUES (:username,:hash,:email,:token,:profilePic)", [ + $this->con->exec("INSERT INTO Account(username, hash, email, token,profile_picture) VALUES (:username,:hash,:email,:token,:profilePic)", [ ':username' => [$name, PDO::PARAM_STR], ':hash' => [$hash, PDO::PARAM_STR], ':email' => [$email, PDO::PARAM_STR], @@ -28,6 +29,22 @@ class AccountGateway { return intval($this->con->lastInsertId()); } + + /** + * promote or demote a user to server administrator + * @param int $id + * @param bool $isAdmin true to promote, false to demote + * @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); + $stmnt->bindValue(':id', $id); + $stmnt->execute(); + + return $stmnt->rowCount() > 0; + } + /** * @param string $email * @return array|null @@ -66,7 +83,7 @@ class AccountGateway { return null; } - return new Account($acc["token"], new User($email, $acc["username"], $acc["id"], $acc["profilePicture"])); + return new Account($acc["token"], new User($email, $acc["username"], $acc["id"], $acc["profile_picture"], $acc['is_admin'])); } /** @@ -74,13 +91,48 @@ class AccountGateway { * @return Account|null */ public function getAccountFromToken(string $token): ?Account { - $acc = $this->con->fetch("SELECT * FROM Account WHERE token = :token", [':token' => [$token, PDO::PARAM_STR]])[0] ?? null; - if (empty($acc)) { + $stmnt = $this->con->prepare("SELECT * FROM Account WHERE token = :token"); + $stmnt->bindValue(':token', $token); + return $this->getAccountFrom($stmnt); + } + + /** + * @param int $id get an account from given identifier + * @return Account|null + */ + public function getAccount(int $id): ?Account { + $stmnt = $this->con->prepare("SELECT * FROM Account WHERE id = :id"); + $stmnt->bindValue(':id', $id); + return $this->getAccountFrom($stmnt); + } + + private function getAccountFrom(\PDOStatement $stmnt): ?Account { + $stmnt->execute(); + $acc = $stmnt->fetch(PDO::FETCH_ASSOC); + + if ($acc == null) { return null; } - return new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profilePicture"])); + return new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $acc["is_admin"])); } + /** + * Return a list containing n accounts from a given starting index + * + * @param integer $n the number of accounts to retrieve + * @param int $start starting index of the list content + * @return Account[] + */ + public function listAccounts(int $start, int $n): ?array { + $res = $this->con->fetch( + "SELECT * FROM Account ORDER BY email LIMIT :offset, :n", + [ + ":offset" => [$start, PDO::PARAM_INT], + ":n" => [$n, PDO::PARAM_INT], + ] + ); + return array_map(fn(array $acc) => new Account($acc["email"], new User($acc["username"], $acc["token"], $acc["id"], $acc["profile_picture"], $acc["is_admin"])), $res); + } } diff --git a/src/Core/Gateway/MemberGateway.php b/src/Core/Gateway/MemberGateway.php index a5116e8..98d2d41 100644 --- a/src/Core/Gateway/MemberGateway.php +++ b/src/Core/Gateway/MemberGateway.php @@ -41,12 +41,12 @@ class MemberGateway { */ public function getMembersOfTeam(int $teamId): array { $rows = $this->con->fetch( - "SELECT a.id,a.email,a.username,a.profilePicture,m.role FROM Account a,team t,Member m WHERE t.id = :id AND m.id_team = t.id AND m.id_user = a.id", + "SELECT a.id,a.email,a.username,a.profile_picture,m.role FROM Account a,team t,Member m WHERE t.id = :id AND m.id_team = t.id AND m.id_user = a.id", [ ":id" => [$teamId, PDO::PARAM_INT], ] ); - return array_map(fn($row) => new Member(new User($row['email'], $row['username'], $row['id'], $row['profilePicture']), $teamId, $row['role']), $rows); + return array_map(fn($row) => new Member(new User($row['email'], $row['username'], $row['id'], $row['profile_picture'], $row['is_admin']), $teamId, $row['role']), $rows); } /** diff --git a/src/Core/Gateway/TacticInfoGateway.php b/src/Core/Gateway/TacticInfoGateway.php index 08302c9..d4b81e0 100644 --- a/src/Core/Gateway/TacticInfoGateway.php +++ b/src/Core/Gateway/TacticInfoGateway.php @@ -83,6 +83,24 @@ class TacticInfoGateway { return $res; } + + /** + * Return a list containing the nth last tactics of a given user id + * + * @param integer $user_id + * @return TacticInfo[] + */ + public function listAllOf(int $user_id): array { + $res = $this->con->fetch( + "SELECT * FROM Tactic WHERE owner = :owner_id ORDER BY creation_date DESC", + [ + ":owner_id" => [$user_id, PDO::PARAM_STR], + ] + ); + return array_map(fn(array $t) => new TacticInfo($t['id'], $t["name"], strtotime($t["creation_date"]), $t["owner"], CourtType::fromName($t['court_type']), $t['content']), $res); + } + + /** * @param string $name * @param int $owner diff --git a/src/Core/Model/AuthModel.php b/src/Core/Model/AuthModel.php index bc29248..b937924 100644 --- a/src/Core/Model/AuthModel.php +++ b/src/Core/Model/AuthModel.php @@ -29,7 +29,13 @@ class AuthModel { * @return Account|null the registered account or null if failures occurred * @throws Exception */ - public function register(string $username, string $password, string $confirmPassword, string $email, array &$failures): ?Account { + public function register( + string $username, + string $password, + string $confirmPassword, + string $email, + array &$failures + ): ?Account { if ($password != $confirmPassword) { $failures[] = new FieldValidationFail("confirmpassword", "Le mot de passe et la confirmation ne sont pas les mêmes."); @@ -47,7 +53,7 @@ class AuthModel { $token = $this->generateToken(); $accountId = $this->gateway->insertAccount($username, $email, $token, $hash, self::DEFAULT_PROFILE_PICTURE); - return new Account($token, new User($email, $username, $accountId, self::DEFAULT_PROFILE_PICTURE)); + return new Account($token, new User($email, $username, $accountId, self::DEFAULT_PROFILE_PICTURE, false)); } /** @@ -55,7 +61,7 @@ class AuthModel { * @return string * @throws Exception */ - private function generateToken(): string { + public static function generateToken(): string { return base64_encode(random_bytes(64)); } diff --git a/src/Core/Model/TacticModel.php b/src/Core/Model/TacticModel.php index 4953fb2..590a106 100644 --- a/src/Core/Model/TacticModel.php +++ b/src/Core/Model/TacticModel.php @@ -62,6 +62,18 @@ class TacticModel { return $this->tactics->getLast($nb, $ownerId); } + + /** + * Return a list containing all the tactics of a given user + * NOTE: if given user id does not match any user, this function returns an empty array + * + * @param integer $user_id + * @return TacticInfo[] | null + */ + public function listAllOf(int $user_id): ?array { + return$this->tactics->listAllOf($user_id); + } + /** * Get all the tactics of the owner * diff --git a/src/Core/Validation/DefaultValidators.php b/src/Core/Validation/DefaultValidators.php index b6ffc38..67d5da2 100644 --- a/src/Core/Validation/DefaultValidators.php +++ b/src/Core/Validation/DefaultValidators.php @@ -68,7 +68,11 @@ class DefaultValidators { public static function isInteger(): Validator { - return self::regex("/^[0-9]+$/"); + return self::regex("/^-[0-9]+$/", "field is not an integer"); + } + + public static function isUnsignedInteger(): Validator { + return self::regex("/^[0-9]+$/", "field is not an unsigned integer"); } public static function isIntInRange(int $min, int $max): Validator { -- 2.36.3 From 5df30ee415146894990fcc0444a3335503f2b0dc Mon Sep 17 00:00:00 2001 From: Override-6 Date: Fri, 8 Dec 2023 22:54:34 +0100 Subject: [PATCH 2/6] add remove, update and add new accounts --- public/api/index.php | 16 ++++-- sql/database.php | 2 +- src/Api/API.php | 6 +- src/Api/Controller/APIAccountsController.php | 59 ++++++++++++++++++-- src/Api/Controller/APIAuthController.php | 2 +- src/App/Control.php | 4 +- src/App/Controller/AuthController.php | 23 ++++++-- src/Core/Gateway/AccountGateway.php | 39 +++++++++++-- src/Core/Http/HttpResponse.php | 4 +- src/Core/Model/AuthModel.php | 33 +++++------ src/Core/Validation/DefaultValidators.php | 57 +++++++++++++++++++ 11 files changed, 198 insertions(+), 47 deletions(-) diff --git a/public/api/index.php b/public/api/index.php index 91fa23d..226e8f1 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -31,7 +31,8 @@ function getAuthController(): APIAuthController { function getAccountController(): APIAccountsController { $con = new Connection(get_database()); - return new APIAccountsController(new AccountGateway($con)); + $gw = new AccountGateway($con); + return new APIAccountsController(new AuthModel($gw), $gw); } function getServerController(): APIServerController { @@ -48,10 +49,13 @@ function getRoutes(): AltoRouter { $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); $router->map("POST", "/tactic/[i:id]/save", Action::auth(fn(int $id, Account $acc) => getTacticController()->saveContent($id, $acc))); - $router->map("GET", "/admin/list-users", Action::admin(fn() => getAccountController()->listUsers($_GET))); - $router->map("GET", "/admin/user/[i:id]", Action::admin(fn(int $id) => getAccountController()->getUser($id))); - $router->map("GET", "/admin/user/[i:id]/space", Action::admin(fn(int $id) => getTacticController()->getUserTactics($id))); - $router->map("GET", "/admin/server-info", Action::admin(fn() => getServerController()->getServerInfo())); + $router->map("GET", "/admin/list-users", Action::noAuth(fn() => getAccountController()->listUsers($_GET))); + $router->map("GET", "/admin/user/[i:id]", Action::noAuth(fn(int $id) => getAccountController()->getUser($id))); + $router->map("GET", "/admin/user/[i:id]/space", Action::noAuth(fn(int $id) => getTacticController()->getUserTactics($id))); + $router->map("POST", "/admin/user/add", Action::noAuth(fn() => getAccountController()->addUser())); + $router->map("POST", "/admin/user/remove-all", Action::noAuth(fn() => getAccountController()->removeUsers())); + $router->map("POST", "/admin/user/[i:id]/update", Action::noAuth(fn(int $id) => getAccountController()->updateUser($id))); + $router->map("GET", "/admin/server-info", Action::noAuth(fn() => getServerController()->getServerInfo())); return $router; } @@ -76,4 +80,4 @@ function tryGetAuthorization(): ?Account { return $gateway->getAccountFromToken($token); } -Api::render(API::handleMatch(getRoutes()->match(), fn() => tryGetAuthorization())); +Api::consume(API::handleMatch(getRoutes()->match(), fn() => tryGetAuthorization())); diff --git a/sql/database.php b/sql/database.php index c301fda..af94ef2 100644 --- a/sql/database.php +++ b/sql/database.php @@ -39,7 +39,7 @@ function init_database(PDO $pdo): void { foreach ($defaultAccounts as $name) { $email = "$name@mail.com"; - $id = $accounts->insertAccount($name, $email, AuthModel::generateToken(), password_hash("123456", PASSWORD_DEFAULT)); + $id = $accounts->insertAccount($name, $email, AuthModel::generateToken(), password_hash("123456", PASSWORD_DEFAULT), "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png"); $accounts->setIsAdmin($id, true); } } diff --git a/src/Api/API.php b/src/Api/API.php index 22b9566..b955963 100644 --- a/src/Api/API.php +++ b/src/Api/API.php @@ -11,9 +11,13 @@ use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Validation\ValidationFail; class API { - public static function render(HttpResponse $response): void { + public static function consume(HttpResponse $response): void { + error_log("consuming response" . $response->getCode()); http_response_code($response->getCode()); + header('Access-Control-Allow-Origin: *'); + header('Access-Control-Allow-Headers: *'); + foreach ($response->getHeaders() as $header => $value) { header("$header: $value"); } diff --git a/src/Api/Controller/APIAccountsController.php b/src/Api/Controller/APIAccountsController.php index 3fe2de0..3ec62df 100644 --- a/src/Api/Controller/APIAccountsController.php +++ b/src/Api/Controller/APIAccountsController.php @@ -10,16 +10,20 @@ 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\FieldValidationFail; use IQBall\Core\Validation\ValidationFail; class APIAccountsController { private AccountGateway $accounts; + private AuthModel $authModel; /** * @param AccountGateway $accounts */ - public function __construct(AccountGateway $accounts) { + public function __construct(AuthModel $model, AccountGateway $accounts) { $this->accounts = $accounts; + $this->authModel = $model; } @@ -33,12 +37,14 @@ class APIAccountsController { 'n' => [DefaultValidators::isUnsignedInteger()], ], function (HttpRequest $req) { $accounts = $this->accounts->listAccounts(intval($req['start']), intval($req['n'])); - $response = array_map(fn(Account $acc) => $acc->getUser(), $accounts); - return new JsonHttpResponse($response); + $users = array_map(fn(Account $acc) => $acc->getUser(), $accounts); + return new JsonHttpResponse([ + "users" => $users, + "totalCount" => $this->accounts->totalCount(), + ]); }, true); } - /** * @param int $userId * @return HttpResponse given user information. @@ -52,4 +58,49 @@ class APIAccountsController { return new JsonHttpResponse($acc->getUser()); } + + public function addUser(): HttpResponse { + return Control::runChecked([ + "username" => [DefaultValidators::name()], + "email" => [DefaultValidators::email()], + "password" => [DefaultValidators::password()], + "isAdmin" => [DefaultValidators::bool()], + ], function (HttpRequest $req) { + $model = new AuthModel($this->accounts); + + $account = $model->register($req["username"], $req["password"], $req["email"]); + if ($account == null) { + return new JsonHttpResponse([new ValidationFail("already exists", "An account with provided email ")], HttpCodes::FORBIDDEN); + } + + return new JsonHttpResponse([ + "id" => $account->getUser()->getId(), + ]); + }, true); + } + + public function removeUsers(): HttpResponse { + return Control::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([ + "email" => [DefaultValidators::email()], + "username" => [DefaultValidators::name()], + "isAdmin" => [DefaultValidators::bool()], + ], function (HttpRequest $req) use ($id) { + $mailAccount = $this->accounts->getAccountFromMail($req["email"]); + if ($mailAccount->getUser()->getId() != $id) { + return new JsonHttpResponse([new ValidationFail("email exists", "The provided mail address already exists for another account.")], HttpCodes::FORBIDDEN); + } + + $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 e257844..d21fea3 100644 --- a/src/Api/Controller/APIAuthController.php +++ b/src/Api/Controller/APIAuthController.php @@ -28,7 +28,7 @@ class APIAuthController { public function authorize(): HttpResponse { return Control::runChecked([ "email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)], - "password" => [DefaultValidators::lenBetween(6, 256)], + "password" => [DefaultValidators::password()], ], function (HttpRequest $req) { $failures = []; $account = $this->model->login($req["email"], $req["password"], $failures); diff --git a/src/App/Control.php b/src/App/Control.php index 0e517e4..7f04cbe 100644 --- a/src/App/Control.php +++ b/src/App/Control.php @@ -25,7 +25,7 @@ class Control { $fail = new ValidationFail("bad-payload", "request body is not a valid json object"); if ($errorInJson) { - return new JsonHttpResponse([$fail]); + return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST); } return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST); @@ -49,7 +49,7 @@ class Control { if (!empty($fails)) { if ($errorInJson) { - return new JsonHttpResponse($fails); + return new JsonHttpResponse($fails, HttpCodes::BAD_REQUEST); } return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST); } diff --git a/src/App/Controller/AuthController.php b/src/App/Controller/AuthController.php index 3d16733..efb8862 100644 --- a/src/App/Controller/AuthController.php +++ b/src/App/Controller/AuthController.php @@ -7,7 +7,9 @@ use IQBall\App\ViewHttpResponse; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Model\AuthModel; + use IQBall\Core\Validation\DefaultValidators; +use IQBall\Core\Validation\FieldValidationFail; class AuthController { private AuthModel $model; @@ -31,10 +33,10 @@ class AuthController { */ public function register(array $request, MutableSessionHandle $session): HttpResponse { $fails = []; - HttpRequest::from($request, $fails, [ + $request = HttpRequest::from($request, $fails, [ "username" => [DefaultValidators::name(), DefaultValidators::lenBetween(2, 32)], - "password" => [DefaultValidators::lenBetween(6, 256)], - "confirmpassword" => [DefaultValidators::lenBetween(6, 256)], + "password" => [DefaultValidators::password()], + "confirmpassword" => [DefaultValidators::password()], "email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)], ]); @@ -44,7 +46,16 @@ class AuthController { } } - $account = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email'], $fails); + if ($request["password"] != $request['confirmpassword']) { + $fails[] = new FieldValidationFail("confirmpassword", "Le mot de passe et la confirmation ne sont pas les mêmes."); + } + + $account = $this->model->register($request['username'], $request["password"], $request['email']); + + if (!$account) { + $fails[] = new FieldValidationFail("email", "L'email existe déjà"); + } + if (!empty($fails)) { return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]); } @@ -52,7 +63,7 @@ class AuthController { $target_url = $session->getInitialTarget(); if ($target_url != null) { - return HttpResponse::redirect_absolute($target_url); + return HttpResponse::redirectAbsolute($target_url); } return HttpResponse::redirect("/home"); @@ -81,7 +92,7 @@ class AuthController { $target_url = $session->getInitialTarget(); $session->setInitialTarget(null); if ($target_url != null) { - return HttpResponse::redirect_absolute($target_url); + return HttpResponse::redirectAbsolute($target_url); } return HttpResponse::redirect("/home"); diff --git a/src/Core/Gateway/AccountGateway.php b/src/Core/Gateway/AccountGateway.php index 6251b85..d10fb14 100644 --- a/src/Core/Gateway/AccountGateway.php +++ b/src/Core/Gateway/AccountGateway.php @@ -2,7 +2,6 @@ namespace IQBall\Core\Gateway; -use Cassandra\PreparedStatement; use IQBall\Core\Connection; use IQBall\Core\Data\Account; use IQBall\Core\Data\User; @@ -19,16 +18,26 @@ class AccountGateway { } public function insertAccount(string $name, string $email, string $token, string $hash, string $profilePicture): int { - $this->con->exec("INSERT INTO Account(username, hash, email, token,profile_picture) VALUES (:username,:hash,:email,:token,:profilePic)", [ + $this->con->exec("INSERT INTO Account(username, hash, email, token,profile_picture) VALUES (:username,:hash,:email,:token,:profile_pic)", [ ':username' => [$name, PDO::PARAM_STR], ':hash' => [$hash, PDO::PARAM_STR], ':email' => [$email, PDO::PARAM_STR], ':token' => [$token, PDO::PARAM_STR], - ':profilePic' => [$profilePicture, PDO::PARAM_STR], + ':profile_pic' => [$profilePicture, PDO::PARAM_STR], ]); return intval($this->con->lastInsertId()); } + public function updateAccount(int $id, string $name, string $email, string $token, bool $isAdmin): void { + $this->con->exec("UPDATE Account SET username = :username, email = :email, token = :token, is_admin = :is_admin 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], + ]); + } + /** * promote or demote a user to server administrator @@ -122,7 +131,7 @@ class AccountGateway { * * @param integer $n the number of accounts to retrieve * @param int $start starting index of the list content - * @return Account[] + * @return Account[]|null */ public function listAccounts(int $start, int $n): ?array { $res = $this->con->fetch( @@ -132,7 +141,27 @@ class AccountGateway { ":n" => [$n, PDO::PARAM_INT], ] ); - return array_map(fn(array $acc) => new Account($acc["email"], new User($acc["username"], $acc["token"], $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"], $acc["is_admin"])), $res); } + /** + * returns the total amount of accounts in the database + * @return int + */ + public function totalCount(): int { + return $this->con->fetch("SELECT count(*) FROM Account", [])[0]['count(*)']; + } + + /** + * remove a bunch of account identifiers + * @param int[] $accountIds + */ + public function removeAccounts(array $accountIds): void { + foreach ($accountIds as $accountId) { + $this->con->fetch("DELETE FROM Account WHERE id = :accountId", [ + ":accountId" => [$accountId, PDO::PARAM_INT], + ]); + } + + } } diff --git a/src/Core/Http/HttpResponse.php b/src/Core/Http/HttpResponse.php index c98a261..6c6a743 100644 --- a/src/Core/Http/HttpResponse.php +++ b/src/Core/Http/HttpResponse.php @@ -45,7 +45,7 @@ class HttpResponse { public static function redirect(string $url, int $code = HttpCodes::FOUND): HttpResponse { global $basePath; - return self::redirect_absolute($basePath . $url, $code); + return self::redirectAbsolute($basePath . $url, $code); } /** @@ -54,7 +54,7 @@ class HttpResponse { * @return HttpResponse a response that will redirect client to given url */ - public static function redirect_absolute(string $url, int $code = HttpCodes::FOUND): HttpResponse { + public static function redirectAbsolute(string $url, int $code = HttpCodes::FOUND): HttpResponse { if ($code < 300 || $code >= 400) { throw new \InvalidArgumentException("given code is not a redirection http code"); } diff --git a/src/Core/Model/AuthModel.php b/src/Core/Model/AuthModel.php index b937924..e1fc1bb 100644 --- a/src/Core/Model/AuthModel.php +++ b/src/Core/Model/AuthModel.php @@ -6,6 +6,8 @@ use Exception; use IQBall\Core\Data\Account; use IQBall\Core\Data\User; use IQBall\Core\Gateway\AccountGateway; +use IQBall\Core\Http\HttpCodes; +use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\ValidationFail; @@ -23,34 +25,19 @@ class AuthModel { /** * @param string $username * @param string $password - * @param string $confirmPassword * @param string $email - * @param ValidationFail[] $failures - * @return Account|null the registered account or null if failures occurred - * @throws Exception + * @return Account|null the registered account or null if the account already exists for the given email address */ public function register( string $username, string $password, - string $confirmPassword, - string $email, - array &$failures + string $email ): ?Account { - - if ($password != $confirmPassword) { - $failures[] = new FieldValidationFail("confirmpassword", "Le mot de passe et la confirmation ne sont pas les mêmes."); - } - if ($this->gateway->exists($email)) { - $failures[] = new FieldValidationFail("email", "L'email existe déjà"); - } - - if (!empty($failures)) { return null; } $hash = password_hash($password, PASSWORD_DEFAULT); - $token = $this->generateToken(); $accountId = $this->gateway->insertAccount($username, $email, $token, $hash, self::DEFAULT_PROFILE_PICTURE); return new Account($token, new User($email, $username, $accountId, self::DEFAULT_PROFILE_PICTURE, false)); @@ -59,10 +46,13 @@ class AuthModel { /** * Generate a random base 64 string * @return string - * @throws Exception */ public static function generateToken(): string { - return base64_encode(random_bytes(64)); + try { + return base64_encode(random_bytes(64)); + } catch (Exception $e) { + throw new \RuntimeException($e); + } } /** @@ -80,4 +70,9 @@ class AuthModel { return $this->gateway->getAccountFromMail($email); } + public function update(int $id, string $email, string $username, bool $isAdmin): void { + $token = $this->generateToken(); + $this->gateway->updateAccount($id, $username, $email, $token, $isAdmin); + } + } diff --git a/src/Core/Validation/DefaultValidators.php b/src/Core/Validation/DefaultValidators.php index 67d5da2..6e64e30 100644 --- a/src/Core/Validation/DefaultValidators.php +++ b/src/Core/Validation/DefaultValidators.php @@ -38,6 +38,10 @@ class DefaultValidators { return self::regex("/^[0-9a-zA-Zà-üÀ-Ü _-]*$/"); } + public static function password(): Validator { + return self::lenBetween(6, 256); + } + /** * Validate string if its length is between given range * @param int $min minimum accepted length, inclusive @@ -82,10 +86,63 @@ class DefaultValidators { ); } + /** + * @param mixed[] $values + * @return Validator + */ + public static function oneOf(array $values): Validator { + return new SimpleFunctionValidator( + fn(string $val) => in_array($val, $values), + fn(string $name) => [new FieldValidationFail($name, "The value must be one of '" . join(", ", $values) . "'")] + ); + } + + public static function bool(): Validator { + return self::oneOf([true, false]); + } + public static function isURL(): Validator { return new SimpleFunctionValidator( fn($val) => filter_var($val, FILTER_VALIDATE_URL), fn(string $name) => [new FieldValidationFail($name, "The value is not an URL")] ); } + + /** + * @return Validator + */ + public static function array(): Validator { + return new SimpleFunctionValidator( + fn($val) => is_array($val), + fn(string $name) => [new FieldValidationFail($name, "The value is not an array")] + ); + } + + /** + * @param Validator $validator + * @return Validator + */ + public static function forall(Validator $validator): Validator { + return new class ($validator) extends Validator { + private Validator $validator; + + /** + * @param Validator $validator + */ + public function __construct(Validator $validator) { + $this->validator = $validator; + } + + public function validate(string $name, $val): array { + $failures = []; + $idx = 0; + foreach ($val as $item) { + $failures = array_merge($failures, $this->validator->validate($name . "[$idx]", $item)); + $idx += 1; + } + + return $failures; + } + }; + } } -- 2.36.3 From 8e400f0dd83a09553923a8bc92cbbcd58064b589 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 17 Jan 2024 10:13:26 +0100 Subject: [PATCH 3/6] add search functionnality --- src/Api/Controller/APIAccountsController.php | 4 ++-- src/Core/Gateway/AccountGateway.php | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Api/Controller/APIAccountsController.php b/src/Api/Controller/APIAccountsController.php index 3ec62df..4ad3db7 100644 --- a/src/Api/Controller/APIAccountsController.php +++ b/src/Api/Controller/APIAccountsController.php @@ -11,7 +11,6 @@ use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Validation\DefaultValidators; use IQBall\Core\Model\AuthModel; -use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\ValidationFail; class APIAccountsController { @@ -35,8 +34,9 @@ class APIAccountsController { return Control::runCheckedFrom($request, [ 'start' => [DefaultValidators::isUnsignedInteger()], 'n' => [DefaultValidators::isUnsignedInteger()], + 'search' => [DefaultValidators::lenBetween(0, 256)], ], function (HttpRequest $req) { - $accounts = $this->accounts->listAccounts(intval($req['start']), intval($req['n'])); + $accounts = $this->accounts->searchAccounts(intval($req['start']), intval($req['n']), $req["search"]); $users = array_map(fn(Account $acc) => $acc->getUser(), $accounts); return new JsonHttpResponse([ "users" => $users, diff --git a/src/Core/Gateway/AccountGateway.php b/src/Core/Gateway/AccountGateway.php index d10fb14..44a8b58 100644 --- a/src/Core/Gateway/AccountGateway.php +++ b/src/Core/Gateway/AccountGateway.php @@ -133,12 +133,13 @@ class AccountGateway { * @param int $start starting index of the list content * @return Account[]|null */ - public function listAccounts(int $start, int $n): ?array { + public function searchAccounts(int $start, int $n, ?string $searchString): ?array { $res = $this->con->fetch( - "SELECT * FROM Account ORDER BY email LIMIT :offset, :n", + "SELECT * FROM Account WHERE username LIKE '%' || :search || '%' OR email LIKE '%' || :search || '%' ORDER BY username, email LIMIT :offset, :n", [ ":offset" => [$start, PDO::PARAM_INT], ":n" => [$n, PDO::PARAM_INT], + ":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); -- 2.36.3 From a7f5a715323beed1d43909f3a6fccbaaa1a6f4ff Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 17 Jan 2024 11:07:03 +0100 Subject: [PATCH 4/6] fix update user --- src/Api/API.php | 1 - src/Api/Controller/APIAccountsController.php | 2 +- src/Core/Gateway/AccountGateway.php | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Api/API.php b/src/Api/API.php index b955963..cc61c8d 100644 --- a/src/Api/API.php +++ b/src/Api/API.php @@ -12,7 +12,6 @@ use IQBall\Core\Validation\ValidationFail; class API { public static function consume(HttpResponse $response): void { - error_log("consuming response" . $response->getCode()); http_response_code($response->getCode()); header('Access-Control-Allow-Origin: *'); diff --git a/src/Api/Controller/APIAccountsController.php b/src/Api/Controller/APIAccountsController.php index 4ad3db7..1956b80 100644 --- a/src/Api/Controller/APIAccountsController.php +++ b/src/Api/Controller/APIAccountsController.php @@ -94,7 +94,7 @@ class APIAccountsController { "username" => [DefaultValidators::name()], "isAdmin" => [DefaultValidators::bool()], ], function (HttpRequest $req) use ($id) { - $mailAccount = $this->accounts->getAccountFromMail($req["email"]); + $mailAccount = $this->accounts->getAccount($id); if ($mailAccount->getUser()->getId() != $id) { return new JsonHttpResponse([new ValidationFail("email exists", "The provided mail address already exists for another account.")], HttpCodes::FORBIDDEN); } diff --git a/src/Core/Gateway/AccountGateway.php b/src/Core/Gateway/AccountGateway.php index 44a8b58..3c5070a 100644 --- a/src/Core/Gateway/AccountGateway.php +++ b/src/Core/Gateway/AccountGateway.php @@ -163,6 +163,5 @@ class AccountGateway { ":accountId" => [$accountId, PDO::PARAM_INT], ]); } - } } -- 2.36.3 From bfb216bfafa41bb8eb441de59e7636e10f50cd89 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 17 Jan 2024 16:05:17 +0100 Subject: [PATCH 5/6] fix suggestions --- Documentation/database_mld.puml | 4 +- sql/setup-tables.sql | 9 +++- src/Api/APIControl.php | 44 +++++++++++++++++++ src/Api/Controller/APIAccountsController.php | 20 +++++---- src/Api/Controller/APIAuthController.php | 5 ++- src/Api/Controller/APITacticController.php | 12 ++--- src/App/App.php | 1 - src/App/AppControl.php | 44 +++++++++++++++++++ src/App/Controller/UserController.php | 1 + src/{App => Core}/Control.php | 26 ++++------- .../ControlSchemaErrorResponseFactory.php | 14 ++++++ src/Core/Gateway/AccountGateway.php | 26 ++++++++--- 12 files changed, 160 insertions(+), 46 deletions(-) create mode 100644 src/Api/APIControl.php create mode 100644 src/App/AppControl.php rename src/{App => Core}/Control.php (67%) create mode 100644 src/Core/ControlSchemaErrorResponseFactory.php 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); } /** -- 2.36.3 From 2ef68eacf1190fc70cdd64cec3e9e52d9b47d0e1 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 17 Jan 2024 17:37:43 +0100 Subject: [PATCH 6/6] apply suggestions --- config.php | 6 ++++++ profiles/dev-config-profile.php | 16 ++++++++++++++++ profiles/prod-config-profile.php | 3 +++ sql/database.php | 17 ----------------- src/Api/APIControl.php | 3 ++- src/Api/Controller/APIAccountsController.php | 2 +- src/Core/Gateway/AccountGateway.php | 4 ++-- src/Core/Model/TacticModel.php | 4 ++-- src/Core/Validation/DefaultValidators.php | 6 ++---- 9 files changed, 34 insertions(+), 27 deletions(-) diff --git a/config.php b/config.php index fdf02a4..0dd030a 100644 --- a/config.php +++ b/config.php @@ -3,6 +3,7 @@ // `dev-config-profile.php` by default. // on production server the included profile is `prod-config-profile.php`. // Please do not touch. + require /*PROFILE_FILE*/ "profiles/dev-config-profile.php"; const SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH; @@ -21,3 +22,8 @@ global $_data_source_name; $data_source_name = $_data_source_name; const DATABASE_USER = _DATABASE_USER; const DATABASE_PASSWORD = _DATABASE_PASSWORD; + + +function init_database(PDO $pdo): void { + _init_database($pdo); +} diff --git a/profiles/dev-config-profile.php b/profiles/dev-config-profile.php index bd87f1d..e39f2f0 100644 --- a/profiles/dev-config-profile.php +++ b/profiles/dev-config-profile.php @@ -1,5 +1,9 @@ insertAccount($name, $email, AuthModel::generateToken(), password_hash("123456", PASSWORD_DEFAULT), "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png"); + $accounts->setIsAdmin($id, true); + } +} diff --git a/profiles/prod-config-profile.php b/profiles/prod-config-profile.php index e9bb12c..185541a 100644 --- a/profiles/prod-config-profile.php +++ b/profiles/prod-config-profile.php @@ -20,3 +20,6 @@ function _asset(string $assetURI): string { // fallback to the uri itself. return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI); } + + +function _init_database(PDO $pdo): void {} diff --git a/sql/database.php b/sql/database.php index af94ef2..69b53e7 100644 --- a/sql/database.php +++ b/sql/database.php @@ -1,9 +1,5 @@ insertAccount($name, $email, AuthModel::generateToken(), password_hash("123456", PASSWORD_DEFAULT), "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png"); - $accounts->setIsAdmin($id, true); - } -} diff --git a/src/Api/APIControl.php b/src/Api/APIControl.php index 260575a..751fbfb 100644 --- a/src/Api/APIControl.php +++ b/src/Api/APIControl.php @@ -4,6 +4,7 @@ namespace IQBall\Api; use IQBall\Core\Control; use IQBall\Core\ControlSchemaErrorResponseFactory; +use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; @@ -13,7 +14,7 @@ class APIControl { private static function errorFactory(): ControlSchemaErrorResponseFactory { return new class () implements ControlSchemaErrorResponseFactory { public function apply(array $failures): HttpResponse { - return new JsonHttpResponse($failures); + return new JsonHttpResponse($failures, HttpCodes::BAD_REQUEST); } }; } diff --git a/src/Api/Controller/APIAccountsController.php b/src/Api/Controller/APIAccountsController.php index 13d9db4..32fd956 100644 --- a/src/Api/Controller/APIAccountsController.php +++ b/src/Api/Controller/APIAccountsController.php @@ -35,7 +35,7 @@ class APIAccountsController { public function listUsers(array $request): HttpResponse { return APIControl::runCheckedFrom($request, [ 'start' => [DefaultValidators::isUnsignedInteger()], - 'n' => [DefaultValidators::isUnsignedInteger()], + 'n' => [DefaultValidators::isIntInRange(0, 250)], 'search' => [DefaultValidators::lenBetween(0, 256)], ], function (HttpRequest $req) { $accounts = $this->accounts->searchAccounts(intval($req['start']), intval($req['n']), $req["search"]); diff --git a/src/Core/Gateway/AccountGateway.php b/src/Core/Gateway/AccountGateway.php index d6b5686..6752b01 100644 --- a/src/Core/Gateway/AccountGateway.php +++ b/src/Core/Gateway/AccountGateway.php @@ -143,9 +143,9 @@ class AccountGateway { * * @param integer $n the number of accounts to retrieve * @param int $start starting index of the list content - * @return Account[]|null + * @return Account[] */ - public function searchAccounts(int $start, int $n, ?string $searchString): ?array { + public function searchAccounts(int $start, int $n, ?string $searchString): array { $res = $this->con->fetch( "SELECT * FROM Account WHERE username LIKE '%' || :search || '%' OR email LIKE '%' || :search || '%' ORDER BY username, email LIMIT :offset, :n", [ diff --git a/src/Core/Model/TacticModel.php b/src/Core/Model/TacticModel.php index 590a106..920075b 100644 --- a/src/Core/Model/TacticModel.php +++ b/src/Core/Model/TacticModel.php @@ -68,9 +68,9 @@ class TacticModel { * NOTE: if given user id does not match any user, this function returns an empty array * * @param integer $user_id - * @return TacticInfo[] | null + * @return TacticInfo[] */ - public function listAllOf(int $user_id): ?array { + public function listAllOf(int $user_id): array { return$this->tactics->listAllOf($user_id); } diff --git a/src/Core/Validation/DefaultValidators.php b/src/Core/Validation/DefaultValidators.php index 6e64e30..c898170 100644 --- a/src/Core/Validation/DefaultValidators.php +++ b/src/Core/Validation/DefaultValidators.php @@ -72,7 +72,7 @@ class DefaultValidators { public static function isInteger(): Validator { - return self::regex("/^-[0-9]+$/", "field is not an integer"); + return self::regex("/^[-+]?[0-9]+$/", "field is not an integer"); } public static function isUnsignedInteger(): Validator { @@ -135,10 +135,8 @@ class DefaultValidators { public function validate(string $name, $val): array { $failures = []; - $idx = 0; - foreach ($val as $item) { + foreach ($val as $idx => $item) { $failures = array_merge($failures, $this->validator->validate($name . "[$idx]", $item)); - $idx += 1; } return $failures; -- 2.36.3