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; + } + }; + } }