Merge pull request 'Add Adminstration API to support accounts management' (#94) from admin/api-accounts into master
continuous-integration/drone/push Build is passing Details

Reviewed-on: #94
pull/96/head
Maxime BATISTA 1 year ago
commit a3e38bded1

@ -5,9 +5,9 @@ object Account {
name name
age age
email email
phoneNumber phone_number
passwordHash password_hash
profilePicture profile_picture
} }
object TacticFolder { object TacticFolder {

@ -3,6 +3,7 @@
// `dev-config-profile.php` by default. // `dev-config-profile.php` by default.
// on production server the included profile is `prod-config-profile.php`. // on production server the included profile is `prod-config-profile.php`.
// Please do not touch. // Please do not touch.
require /*PROFILE_FILE*/ "profiles/dev-config-profile.php"; require /*PROFILE_FILE*/ "profiles/dev-config-profile.php";
const SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH; const SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH;
@ -21,3 +22,8 @@ global $_data_source_name;
$data_source_name = $_data_source_name; $data_source_name = $_data_source_name;
const DATABASE_USER = _DATABASE_USER; const DATABASE_USER = _DATABASE_USER;
const DATABASE_PASSWORD = _DATABASE_PASSWORD; const DATABASE_PASSWORD = _DATABASE_PASSWORD;
function init_database(PDO $pdo): void {
_init_database($pdo);
}

@ -1,5 +1,9 @@
<?php <?php
use IQBall\Core\Connection;
use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Model\AuthModel;
$hostname = getHostName(); $hostname = getHostName();
$front_url = "http://$hostname:5173"; $front_url = "http://$hostname:5173";
@ -14,3 +18,15 @@ function _asset(string $assetURI): string {
global $front_url; global $front_url;
return $front_url . "/" . $assetURI; return $front_url . "/" . $assetURI;
} }
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), "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png");
$accounts->setIsAdmin($id, true);
}
}

@ -20,3 +20,6 @@ function _asset(string $assetURI): string {
// fallback to the uri itself. // fallback to the uri itself.
return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI); return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI);
} }
function _init_database(PDO $pdo): void {}

@ -6,7 +6,9 @@ require "../../sql/database.php";
require "../../src/index-utils.php"; require "../../src/index-utils.php";
use IQBall\Api\API; use IQBall\Api\API;
use IQBall\Api\Controller\APIAccountsController;
use IQBall\Api\Controller\APIAuthController; use IQBall\Api\Controller\APIAuthController;
use IQBall\Api\Controller\APIServerController;
use IQBall\Api\Controller\APITacticController; use IQBall\Api\Controller\APITacticController;
use IQBall\App\Session\PhpSessionHandle; use IQBall\App\Session\PhpSessionHandle;
use IQBall\Core\Action; use IQBall\Core\Action;
@ -17,6 +19,8 @@ use IQBall\Core\Gateway\TacticInfoGateway;
use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\AuthModel;
use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TacticModel;
$basePath = get_public_path(__DIR__);
function getTacticController(): APITacticController { function getTacticController(): APITacticController {
return new APITacticController(new TacticModel(new TacticInfoGateway(new Connection(get_database())))); return new APITacticController(new TacticModel(new TacticInfoGateway(new Connection(get_database()))));
} }
@ -25,14 +29,34 @@ function getAuthController(): APIAuthController {
return new APIAuthController(new AuthModel(new AccountGateway(new Connection(get_database())))); return new APIAuthController(new AuthModel(new AccountGateway(new Connection(get_database()))));
} }
function getAccountController(): APIAccountsController {
$con = new Connection(get_database());
$gw = new AccountGateway($con);
return new APIAccountsController(new AuthModel($gw), $gw);
}
function getServerController(): APIServerController {
global $basePath;
return new APIServerController($basePath, get_database());
}
function getRoutes(): AltoRouter { function getRoutes(): AltoRouter {
$router = new 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", "/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]/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("POST", "/tactic/[i:id]/save", Action::auth(fn(int $id, Account $acc) => getTacticController()->saveContent($id, $acc)));
$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; return $router;
} }
@ -56,4 +80,4 @@ function tryGetAuthorization(): ?Account {
return $gateway->getAccountFromToken($token); return $gateway->getAccountFromToken($token);
} }
Api::render(API::handleMatch(getRoutes()->match(), fn() => tryGetAuthorization())); Api::consume(API::handleMatch(getRoutes()->match(), fn() => tryGetAuthorization()));

@ -22,6 +22,7 @@ function get_database(): PDO {
} }
} }
init_database($pdo);
return $pdo; return $pdo;
} }

@ -4,6 +4,12 @@ DROP TABLE IF EXISTS Tactic;
DROP TABLE IF EXISTS Team; DROP TABLE IF EXISTS Team;
DROP TABLE IF EXISTS User; DROP TABLE IF EXISTS User;
DROP TABLE IF EXISTS Member; DROP TABLE IF EXISTS Member;
CREATE TABLE Admins
(
id integer PRIMARY KEY REFERENCES Account
);
CREATE TABLE Account CREATE TABLE Account
( (
id integer PRIMARY KEY AUTOINCREMENT, id integer PRIMARY KEY AUTOINCREMENT,
@ -11,7 +17,7 @@ CREATE TABLE Account
username varchar NOT NULL, username varchar NOT NULL,
token varchar UNIQUE NOT NULL, token varchar UNIQUE NOT NULL,
hash varchar NOT NULL, hash varchar NOT NULL,
profilePicture varchar NOT NULL profile_picture varchar NOT NULL
); );
CREATE TABLE Tactic CREATE TABLE Tactic
@ -25,13 +31,6 @@ CREATE TABLE Tactic
FOREIGN KEY (owner) REFERENCES Account FOREIGN KEY (owner) REFERENCES Account
); );
CREATE TABLE FormEntries
(
name varchar NOT NULL,
description varchar NOT NULL
);
CREATE TABLE Team CREATE TABLE Team
( (
id integer PRIMARY KEY AUTOINCREMENT NOT NULL, id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
@ -41,11 +40,10 @@ CREATE TABLE Team
second_color varchar NOT NULL second_color varchar NOT NULL
); );
CREATE TABLE Member CREATE TABLE Member
( (
id_team integer NOT NULL, id_team integer NOT NULL,
id_user integer NOT NULL, id_user integer NOT NULL,
role text CHECK (role IN ('COACH', 'PLAYER')) NOT NULL, role text CHECK (role IN ('COACH', 'PLAYER')) NOT NULL,
FOREIGN KEY (id_team) REFERENCES Team (id), FOREIGN KEY (id_team) REFERENCES Team (id),
FOREIGN KEY (id_user) REFERENCES Account (id) FOREIGN KEY (id_user) REFERENCES Account (id)

@ -4,14 +4,19 @@ namespace IQBall\Api;
use Exception; use Exception;
use IQBall\Core\Action; use IQBall\Core\Action;
use IQBall\Core\Data\Account;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\ValidationFail;
class API { class API {
public static function render(HttpResponse $response): void { public static function consume(HttpResponse $response): void {
http_response_code($response->getCode()); http_response_code($response->getCode());
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: *');
foreach ($response->getHeaders() as $header => $value) { foreach ($response->getHeaders() as $header => $value) {
header("$header: $value"); header("$header: $value");
} }
@ -27,7 +32,7 @@ class API {
/** /**
* @param array<string, mixed>|false $match * @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 * @return HttpResponse
* @throws Exception * @throws Exception
*/ */
@ -41,15 +46,19 @@ class API {
throw new Exception("routed action is not an AppAction object."); throw new Exception("routed action is not an AppAction object.");
} }
$auth = null; $account = null;
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->isAuthRequired()) { if ($action->getAuthType() == Action::AUTH_ADMIN && !$account->getUser()->isAdmin()) {
$auth = call_user_func($tryGetAuthorization); return new JsonHttpResponse([ValidationFail::unauthorized()], HttpCodes::UNAUTHORIZED);
if ($auth == null) {
return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")]);
} }
} }
return $action->run($match['params'], $auth); return $action->run($match['params'], $account);
} }
} }

@ -0,0 +1,45 @@
<?php
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;
use IQBall\Core\Validation\Validator;
class APIControl {
private static function errorFactory(): ControlSchemaErrorResponseFactory {
return new class () implements ControlSchemaErrorResponseFactory {
public function apply(array $failures): HttpResponse {
return new JsonHttpResponse($failures, HttpCodes::BAD_REQUEST);
}
};
}
/**
* Runs given callback, if the request's payload 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.
* @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<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.
* @return HttpResponse
*/
public static function runCheckedFrom(array $data, array $schema, callable $run): HttpResponse {
return Control::runCheckedFrom($data, $schema, $run, self::errorFactory());
}
}

@ -0,0 +1,108 @@
<?php
namespace IQBall\Api\Controller;
use IQBall\Api\APIControl;
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\Model\AuthModel;
use IQBall\Core\Validation\DefaultValidators;
use IQBall\Core\Validation\ValidationFail;
class APIAccountsController {
private AccountGateway $accounts;
private AuthModel $authModel;
/**
* @param AuthModel $model
* @param AccountGateway $accounts
*/
public function __construct(AuthModel $model, AccountGateway $accounts) {
$this->accounts = $accounts;
$this->authModel = $model;
}
/**
* @param array<string, mixed> $request
* @return HttpResponse
*/
public function listUsers(array $request): HttpResponse {
return APIControl::runCheckedFrom($request, [
'start' => [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"]);
$users = array_map(fn(Account $acc) => $acc->getUser(), $accounts);
return new JsonHttpResponse([
"users" => $users,
"totalCount" => $this->accounts->totalCount(),
]);
});
}
/**
* @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());
}
public function addUser(): HttpResponse {
return APIControl::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(),
]);
});
}
public function removeUsers(): HttpResponse {
return APIControl::runChecked([
"identifiers" => [DefaultValidators::array(), DefaultValidators::forall(DefaultValidators::isUnsignedInteger())],
], function (HttpRequest $req) {
$this->accounts->removeAccounts($req["identifiers"]);
return HttpResponse::fromCode(HttpCodes::OK);
});
}
public function updateUser(int $id): HttpResponse {
return APIControl::runChecked([
"email" => [DefaultValidators::email()],
"username" => [DefaultValidators::name()],
"isAdmin" => [DefaultValidators::bool()],
], function (HttpRequest $req) use ($id) {
$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);
}
$this->authModel->update($id, $req["email"], $req["username"], $req["isAdmin"]);
return HttpResponse::fromCode(HttpCodes::OK);
});
}
}

@ -2,6 +2,7 @@
namespace IQBall\Api\Controller; namespace IQBall\Api\Controller;
use IQBall\Api\APIControl;
use IQBall\App\Control; use IQBall\App\Control;
use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpRequest;
@ -26,9 +27,9 @@ class APIAuthController {
* @return HttpResponse * @return HttpResponse
*/ */
public function authorize(): HttpResponse { public function authorize(): HttpResponse {
return Control::runChecked([ return APIControl::runChecked([
"email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)], "email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)],
"password" => [DefaultValidators::lenBetween(6, 256)], "password" => [DefaultValidators::password()],
], function (HttpRequest $req) { ], function (HttpRequest $req) {
$failures = []; $failures = [];
$account = $this->model->login($req["email"], $req["password"], $failures); $account = $this->model->login($req["email"], $req["password"], $failures);
@ -40,5 +41,4 @@ class APIAuthController {
return new JsonHttpResponse(["authorization" => $account->getToken()]); return new JsonHttpResponse(["authorization" => $account->getToken()]);
}); });
} }
} }

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

@ -2,14 +2,14 @@
namespace IQBall\Api\Controller; namespace IQBall\Api\Controller;
use IQBall\App\Control; use IQBall\Api\APIControl;
use IQBall\Core\Data\Account; use IQBall\Core\Data\Account;
use IQBall\Core\Data\TacticInfo;
use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TacticModel;
use IQBall\Core\Validation\FieldValidationFail;
use IQBall\Core\Validation\DefaultValidators; use IQBall\Core\Validation\DefaultValidators;
/** /**
@ -32,7 +32,7 @@ class APITacticController {
* @return HttpResponse * @return HttpResponse
*/ */
public function updateName(int $tactic_id, Account $account): HttpResponse { public function updateName(int $tactic_id, Account $account): HttpResponse {
return Control::runChecked([ return APIControl::runChecked([
"name" => [DefaultValidators::lenBetween(1, 50), DefaultValidators::nameWithSpaces()], "name" => [DefaultValidators::lenBetween(1, 50), DefaultValidators::nameWithSpaces()],
], function (HttpRequest $request) use ($tactic_id, $account) { ], function (HttpRequest $request) use ($tactic_id, $account) {
@ -49,17 +49,37 @@ class APITacticController {
/** /**
* @param int $id * @param int $id
* @param Account $account
* @return HttpResponse * @return HttpResponse
*/ */
public function saveContent(int $id, Account $account): HttpResponse { public function saveContent(int $id, Account $account): HttpResponse {
return Control::runChecked([ return APIControl::runChecked([
"content" => [], "content" => [],
], function (HttpRequest $req) use ($id) { ], 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"]))) { if ($fail = $this->model->updateContent($id, json_encode($req["content"]))) {
return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST); return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST);
} }
return HttpResponse::fromCode(HttpCodes::OK); return HttpResponse::fromCode(HttpCodes::OK);
}); });
} }
/**
* @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\App\Session\MutableSessionHandle;
use IQBall\Core\Action; use IQBall\Core\Action;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Validation\ValidationFail;
use Twig\Environment; use Twig\Environment;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
use Twig\Error\RuntimeError; use Twig\Error\RuntimeError;
@ -77,13 +79,17 @@ class App {
* @return HttpResponse * @return HttpResponse
*/ */
public static function runAction(string $authRoute, Action $action, array $params, MutableSessionHandle $session): 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(); $account = $session->getAccount();
if ($account == null) { if ($account == null) {
// put in the session the initial url the user wanted to get // put in the session the initial url the user wanted to get
$session->setInitialTarget($_SERVER['REQUEST_URI']); $session->setInitialTarget($_SERVER['REQUEST_URI']);
return HttpResponse::redirect($authRoute); 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); return $action->run($params, $session);

@ -0,0 +1,44 @@
<?php
namespace IQBall\App;
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\Validation\Validator;
class AppControl {
private static function errorFactory(): ControlSchemaErrorResponseFactory {
return new class () implements ControlSchemaErrorResponseFactory {
public function apply(array $failures): HttpResponse {
return ViewHttpResponse::twig("error.html.twig", ['failures' => $failures], HttpCodes::BAD_REQUEST);
}
};
}
/**
* Runs given callback, if the request's payload 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.
* @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<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.
* @return HttpResponse
*/
public static function runCheckedFrom(array $data, array $schema, callable $run): HttpResponse {
return Control::runCheckedFrom($data, $schema, $run, self::errorFactory());
}
}

@ -7,7 +7,9 @@ use IQBall\App\ViewHttpResponse;
use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\AuthModel;
use IQBall\Core\Validation\DefaultValidators; use IQBall\Core\Validation\DefaultValidators;
use IQBall\Core\Validation\FieldValidationFail;
class AuthController { class AuthController {
private AuthModel $model; private AuthModel $model;
@ -31,10 +33,10 @@ class AuthController {
*/ */
public function register(array $request, MutableSessionHandle $session): HttpResponse { public function register(array $request, MutableSessionHandle $session): HttpResponse {
$fails = []; $fails = [];
HttpRequest::from($request, $fails, [ $request = HttpRequest::from($request, $fails, [
"username" => [DefaultValidators::name(), DefaultValidators::lenBetween(2, 32)], "username" => [DefaultValidators::name(), DefaultValidators::lenBetween(2, 32)],
"password" => [DefaultValidators::lenBetween(6, 256)], "password" => [DefaultValidators::password()],
"confirmpassword" => [DefaultValidators::lenBetween(6, 256)], "confirmpassword" => [DefaultValidators::password()],
"email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)], "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)) { if (!empty($fails)) {
return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]); return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]);
} }
@ -52,7 +63,7 @@ class AuthController {
$target_url = $session->getInitialTarget(); $target_url = $session->getInitialTarget();
if ($target_url != null) { if ($target_url != null) {
return HttpResponse::redirect_absolute($target_url); return HttpResponse::redirectAbsolute($target_url);
} }
return HttpResponse::redirect("/home"); return HttpResponse::redirect("/home");
@ -81,7 +92,7 @@ class AuthController {
$target_url = $session->getInitialTarget(); $target_url = $session->getInitialTarget();
$session->setInitialTarget(null); $session->setInitialTarget(null);
if ($target_url != null) { if ($target_url != null) {
return HttpResponse::redirect_absolute($target_url); return HttpResponse::redirectAbsolute($target_url);
} }
return HttpResponse::redirect("/home"); return HttpResponse::redirect("/home");

@ -5,6 +5,7 @@ namespace IQBall\App\Controller;
use IQBall\App\Session\MutableSessionHandle; use IQBall\App\Session\MutableSessionHandle;
use IQBall\App\Session\SessionHandle; use IQBall\App\Session\SessionHandle;
use IQBall\App\ViewHttpResponse; use IQBall\App\ViewHttpResponse;
use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TacticModel;
use IQBall\Core\Model\TeamModel; use IQBall\Core\Model\TeamModel;

@ -9,23 +9,27 @@ use IQBall\Core\Http\HttpResponse;
* @template S session * @template S session
*/ */
class Action { 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 * @var callable(mixed[], S): HttpResponse $action action to call
*/ */
protected $action; protected $action;
private bool $isAuthRequired; private int $authType;
/** /**
* @param callable(mixed[], S): HttpResponse $action * @param callable(mixed[], S): HttpResponse $action
*/ */
protected function __construct(callable $action, bool $isAuthRequired) { protected function __construct(callable $action, int $authType) {
$this->action = $action; $this->action = $action;
$this->isAuthRequired = $isAuthRequired; $this->authType = $authType;
} }
public function isAuthRequired(): bool { public function getAuthType(): int {
return $this->isAuthRequired; return $this->authType;
} }
/** /**
@ -45,7 +49,7 @@ class Action {
* @return Action<S> an action that does not require to have an authorization. * @return Action<S> an action that does not require to have an authorization.
*/ */
public static function noAuth(callable $action): Action { 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. * @return Action<S> an action that does require to have an authorization.
*/ */
public static function auth(callable $action): Action { 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);
} }
} }

@ -1,30 +1,32 @@
<?php <?php
namespace IQBall\App; namespace IQBall\Core;
use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\ValidationFail;
use IQBall\Core\Validation\Validator; use IQBall\Core\Validation\Validator;
class Control { class Control {
/** /**
* Runs given callback, if the request's json validates the given schema. * Runs given callback, if the request's payload json validates the given schema.
* @param array<string, Validator[]> $schema an array of `fieldName => DefaultValidators` which represents the request object 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. * @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 ControlSchemaErrorResponseFactory $errorFactory an error factory to use if the request does not validate the required schema
* @return HttpResponse * @return HttpResponse
*/ */
public static function runChecked(array $schema, callable $run): HttpResponse { public static function runChecked(array $schema, callable $run, ControlSchemaErrorResponseFactory $errorFactory): HttpResponse {
$request_body = file_get_contents('php://input'); $request_body = file_get_contents('php://input');
$payload_obj = json_decode($request_body); $payload_obj = json_decode($request_body);
if (!$payload_obj instanceof \stdClass) { if (!$payload_obj instanceof \stdClass) {
$fail = new ValidationFail("bad-payload", "request body is not a valid json object"); $fail = new ValidationFail("bad-payload", "request body is not a valid json object");
return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST); return $errorFactory->apply([$fail]);
} }
$payload = get_object_vars($payload_obj); $payload = get_object_vars($payload_obj);
return self::runCheckedFrom($payload, $schema, $run); return self::runCheckedFrom($payload, $schema, $run, $errorFactory);
} }
/** /**
@ -32,15 +34,16 @@ class Control {
* @param array<string, mixed> $data the request's data array. * @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 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. * @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 ControlSchemaErrorResponseFactory $errorFactory an error factory to use if the request does not validate the required schema
* @return HttpResponse * @return HttpResponse
*/ */
public static function runCheckedFrom(array $data, array $schema, callable $run): HttpResponse { public static function runCheckedFrom(array $data, array $schema, callable $run, ControlSchemaErrorResponseFactory $errorFactory): HttpResponse {
$fails = []; $fails = [];
$request = HttpRequest::from($data, $fails, $schema); $request = HttpRequest::from($data, $fails, $schema);
if (!empty($fails)) { if (!empty($fails)) {
return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST); return $errorFactory->apply($fails);
} }
return call_user_func_array($run, [$request]); return call_user_func_array($run, [$request]);

@ -0,0 +1,14 @@
<?php
namespace IQBall\Core;
use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Validation\ValidationFail;
interface ControlSchemaErrorResponseFactory {
/**
* @param ValidationFail[] $failures
* @return HttpResponse
*/
public function apply(array $failures): HttpResponse;
}

@ -2,8 +2,6 @@
namespace IQBall\Core\Data; namespace IQBall\Core\Data;
use _PHPStan_4c4f22f13\Nette\Utils\Json;
class User implements \JsonSerializable { class User implements \JsonSerializable {
/** /**
* @var string $email user's mail address * @var string $email user's mail address
@ -25,17 +23,31 @@ class User implements \JsonSerializable {
*/ */
private string $profilePicture; private string $profilePicture;
/**
* @var bool isAdmin
*/
private bool $isAdmin;
/** /**
* @param string $email * @param string $email
* @param string $name * @param string $name
* @param int $id * @param int $id
* @param string $profilePicture * @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->email = $email;
$this->name = $name; $this->name = $name;
$this->id = $id; $this->id = $id;
$this->profilePicture = $profilePicture; $this->profilePicture = $profilePicture;
$this->isAdmin = $isAdmin;
}
/**
* @return bool
*/
public function isAdmin(): bool {
return $this->isAdmin;
} }
/** /**

@ -18,16 +18,54 @@ class AccountGateway {
} }
public function insertAccount(string $name, string $email, string $token, string $hash, string $profilePicture): int { 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,:profile_pic)", [
':username' => [$name, PDO::PARAM_STR], ':username' => [$name, PDO::PARAM_STR],
':hash' => [$hash, PDO::PARAM_STR], ':hash' => [$hash, PDO::PARAM_STR],
':email' => [$email, PDO::PARAM_STR], ':email' => [$email, PDO::PARAM_STR],
':token' => [$token, PDO::PARAM_STR], ':token' => [$token, PDO::PARAM_STR],
':profilePic' => [$profilePicture, PDO::PARAM_STR], ':profile_pic' => [$profilePicture, PDO::PARAM_STR],
]); ]);
return intval($this->con->lastInsertId()); 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 WHERE id = :id", [
':username' => [$name, PDO::PARAM_STR],
':email' => [$email, PDO::PARAM_STR],
':token' => [$token, PDO::PARAM_STR],
':id' => [$id, PDO::PARAM_INT],
]);
$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);
}
/**
* 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 {
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();
return $stmnt->rowCount() > 0;
}
/** /**
* @param string $email * @param string $email
* @return array<string, mixed>|null * @return array<string, mixed>|null
@ -66,7 +104,7 @@ class AccountGateway {
return null; 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"], $this->isAdmin($acc["id"])));
} }
/** /**
@ -74,13 +112,68 @@ class AccountGateway {
* @return Account|null * @return Account|null
*/ */
public function getAccountFromToken(string $token): ?Account { public function getAccountFromToken(string $token): ?Account {
$acc = $this->con->fetch("SELECT * FROM Account WHERE token = :token", [':token' => [$token, PDO::PARAM_STR]])[0] ?? null; $stmnt = $this->con->prepare("SELECT * FROM Account WHERE token = :token");
if (empty($acc)) { $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 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"], $this->isAdmin($acc["id"])));
}
/**
* 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 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",
[
":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"], $this->isAdmin($acc["id"]))), $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],
]);
}
}
} }

@ -41,12 +41,12 @@ class MemberGateway {
*/ */
public function getMembersOfTeam(int $teamId): array { public function getMembersOfTeam(int $teamId): array {
$rows = $this->con->fetch( $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], ":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 $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 string $name
* @param int $owner * @param int $owner

@ -45,7 +45,7 @@ class HttpResponse {
public static function redirect(string $url, int $code = HttpCodes::FOUND): HttpResponse { public static function redirect(string $url, int $code = HttpCodes::FOUND): HttpResponse {
global $basePath; 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 * @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) { if ($code < 300 || $code >= 400) {
throw new \InvalidArgumentException("given code is not a redirection http code"); throw new \InvalidArgumentException("given code is not a redirection http code");
} }

@ -6,6 +6,8 @@ use Exception;
use IQBall\Core\Data\Account; use IQBall\Core\Data\Account;
use IQBall\Core\Data\User; use IQBall\Core\Data\User;
use IQBall\Core\Gateway\AccountGateway; use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\FieldValidationFail;
use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\ValidationFail;
@ -23,40 +25,34 @@ class AuthModel {
/** /**
* @param string $username * @param string $username
* @param string $password * @param string $password
* @param string $confirmPassword
* @param string $email * @param string $email
* @param ValidationFail[] $failures * @return Account|null the registered account or null if the account already exists for the given email address
* @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,
if ($password != $confirmPassword) { string $password,
$failures[] = new FieldValidationFail("confirmpassword", "Le mot de passe et la confirmation ne sont pas les mêmes."); string $email
} ): ?Account {
if ($this->gateway->exists($email)) { if ($this->gateway->exists($email)) {
$failures[] = new FieldValidationFail("email", "L'email existe déjà");
}
if (!empty($failures)) {
return null; return null;
} }
$hash = password_hash($password, PASSWORD_DEFAULT); $hash = password_hash($password, PASSWORD_DEFAULT);
$token = $this->generateToken(); $token = $this->generateToken();
$accountId = $this->gateway->insertAccount($username, $email, $token, $hash, self::DEFAULT_PROFILE_PICTURE); $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));
} }
/** /**
* Generate a random base 64 string * Generate a random base 64 string
* @return string * @return string
* @throws Exception
*/ */
private function generateToken(): string { 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);
}
} }
/** /**
@ -74,4 +70,9 @@ class AuthModel {
return $this->gateway->getAccountFromMail($email); 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);
}
} }

@ -62,6 +62,18 @@ class TacticModel {
return $this->tactics->getLast($nb, $ownerId); 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[]
*/
public function listAllOf(int $user_id): array {
return$this->tactics->listAllOf($user_id);
}
/** /**
* Get all the tactics of the owner * Get all the tactics of the owner
* *

@ -38,6 +38,10 @@ class DefaultValidators {
return self::regex("/^[0-9a-zA-Zà-üÀ-Ü _-]*$/"); 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 * Validate string if its length is between given range
* @param int $min minimum accepted length, inclusive * @param int $min minimum accepted length, inclusive
@ -68,7 +72,11 @@ class DefaultValidators {
public static function isInteger(): Validator { 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 { public static function isIntInRange(int $min, int $max): Validator {
@ -78,10 +86,61 @@ 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 { public static function isURL(): Validator {
return new SimpleFunctionValidator( return new SimpleFunctionValidator(
fn($val) => filter_var($val, FILTER_VALIDATE_URL), fn($val) => filter_var($val, FILTER_VALIDATE_URL),
fn(string $name) => [new FieldValidationFail($name, "The value is not an 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 = [];
foreach ($val as $idx => $item) {
$failures = array_merge($failures, $this->validator->validate($name . "[$idx]", $item));
}
return $failures;
}
};
}
} }

Loading…
Cancel
Save