add basic api routes to get info on server, users and tactics

pull/94/head
Override-6 2 years ago committed by maxime.batista
parent 3ace793578
commit 9cde3af510

@ -7,7 +7,7 @@ object Account {
email
phoneNumber
passwordHash
profilePicture
profile_picture
}
object TacticFolder {

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

@ -1,5 +1,9 @@
<?php
use IQBall\Core\Connection;
use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Model\AuthModel;
/**
* @return PDO The PDO instance of the configuration's database connexion.
*/
@ -22,6 +26,20 @@ function get_database(): PDO {
}
}
init_database($pdo);
return $pdo;
}
function init_database(PDO $pdo): void {
$accounts = new AccountGateway(new Connection($pdo));
$defaultAccounts = ["maxime", "mael", "yanis", "vivien"];
foreach ($defaultAccounts as $name) {
$email = "$name@mail.com";
$id = $accounts->insertAccount($name, $email, AuthModel::generateToken(), password_hash("123456", PASSWORD_DEFAULT));
$accounts->setIsAdmin($id, true);
}
}

@ -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,7 +35,6 @@ CREATE TABLE Team
second_color varchar NOT NULL
);
CREATE TABLE Member
(
id_team integer NOT NULL,

@ -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<string, mixed>|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);
}
}

@ -0,0 +1,55 @@
<?php
namespace IQBall\Api\Controller;
use IQBall\App\Control;
use IQBall\Core\Data\Account;
use IQBall\Core\Gateway\AccountGateway;
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\Validation\ValidationFail;
class APIAccountsController {
private AccountGateway $accounts;
/**
* @param AccountGateway $accounts
*/
public function __construct(AccountGateway $accounts) {
$this->accounts = $accounts;
}
/**
* @param array<string, mixed> $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());
}
}

@ -38,7 +38,6 @@ class APIAuthController {
}
return new JsonHttpResponse(["authorization" => $account->getToken()]);
});
}, true);
}
}

@ -0,0 +1,45 @@
<?php
namespace IQBall\Api\Controller;
use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse;
class APIServerController {
private string $basePath;
private \PDO $pdo;
/**
* @param string $basePath
* @param \PDO $pdo
*/
public function __construct(string $basePath, \PDO $pdo) {
$this->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)",
],
]);
}
}

@ -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);
}
}

@ -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);

@ -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<string, Validator[]> $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<string, mixed> $data the request's data array.
* @param array<string, Validator[]> $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);
}

@ -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<S> 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<S> 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<S> 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);
}
}

@ -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;
}
/**

@ -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<string, mixed>|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);
}
}

@ -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);
}
/**

@ -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

@ -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));
}

@ -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
*

@ -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 {

Loading…
Cancel
Save