add and edit documentation, clean code
continuous-integration/drone/push Build is passing Details

pull/22/head
Override-6 1 year ago
parent 49cdcb8972
commit 89c3e72c8f
Signed by untrusted user who does not match committer: maxime.batista
GPG Key ID: 8002CC4B4DD9ECA5

@ -28,7 +28,7 @@ If we take a look at the request, we'll see that the url does not targets `local
`localhost:5173` is the react development server, it is able to serve our react front view files. `localhost:5173` is the react development server, it is able to serve our react front view files.
Let's run the react development server. Let's run the react development server.
It is a simple as running `npm start` in a new terminal (be sure to run it in the repository's directory). It is as simple as running `npm start` in a new terminal (be sure to run it in the repository's directory).
![](assets/npm-start.png) ![](assets/npm-start.png)
You should see something like this, it says that the server was opened on port `5173`, thats our react development server ! You should see something like this, it says that the server was opened on port `5173`, thats our react development server !
@ -40,7 +40,7 @@ Caution: **NEVER** directly connect on the `localhost:5173` node development ser
# How it works # How it works
I'm glad you are interested in how that stuff works, it's a bit tricky, lets go. I'm glad you are interested in how that stuff works, it's a bit tricky, lets go.
If you look at our `index.php` (located in `/public` folder), you'll see that it is our gateway, it uses an `AltoRouter` that dispatches the request's process to a controller. If you look at our `index.php` (located in `/public` folder), you'll see that it define our routes, it uses an `AltoRouter` then delegates the request's action processing to a controller.
We can see that there are two registered routes, the `GET:/` (that then calls `SampleFormController#displayForm()`) and `POST:/result` (that calls `SampleFormController#displayResults()`). We can see that there are two registered routes, the `GET:/` (that then calls `SampleFormController#displayForm()`) and `POST:/result` (that calls `SampleFormController#displayResults()`).
Implementation of those two methods are very simple: there is no verification to make nor model to control, thus they directly sends the view back to the client. Implementation of those two methods are very simple: there is no verification to make nor model to control, thus they directly sends the view back to the client.
@ -115,7 +115,7 @@ function _asset(string $assetURI): string {
The simplest profile, simply redirect all assets to the development server The simplest profile, simply redirect all assets to the development server
### Production profile ### Production profile
Before the CD deploys the generated files to the server, Before the CD workflow step deploys the generated files to the server,
it generates a `/views-mappings.php` file that will map the react views file names to their compiled javascript files : it generates a `/views-mappings.php` file that will map the react views file names to their compiled javascript files :
```php ```php
@ -137,13 +137,4 @@ 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);
} }
``` ```
## React views conventions.
Conventions regarding our react views __must be respected in order to be renderable__.
### The `render(any)` function
Any React view component needs to be default exported in order to be imported and used from PHP. Those components will receive as props the arguments that the PHP server has transmitted.
The `arguments` parameter is used to pass data to the react component.
If you take a look at the `front/views/SampleForm.tsx` view, here's the definition of its render function :

@ -44,4 +44,8 @@ class ViewHttpResponse extends HttpResponse {
+ <u>react(file: string, arguments: array, code: int = HttpCodes::OK): ViewHttpResponse + <u>react(file: string, arguments: array, code: int = HttpCodes::OK): ViewHttpResponse
} }
note right of ViewHttpResponse
Into src/App
end note
@enduml @enduml

@ -1,45 +1,36 @@
@startuml @startuml
class Account { class TacticInfo {
- email: string
- phoneNumber: string
- id: int - id: int
- name: string
+ __construct (email : string, ???) - creationDate: string
+ setMailAddress(string) - ownerId: string
+ getMailAddress(): string
+ getPhoneNumber(): string
+ setPhoneNumber(string)
+ getUser(): AccountUser
+ getId(): int + getId(): int
} + getOwnerId(): int
+ getCreationTimestamp(): int
Account --> "- user" AccountUser
Account --> "- teams *" Team
interface User {
+ getName(): string + getName(): string
+ getProfilePicture(): Url
+ getAge(): int
} }
class AccountUser { class Account {
- email: string
- token: string
- name: string - name: string
- profilePicture: Url - id: int
- age: int
+ __construct(name : string, age : int, profilePicture : string) + getMailAddress(): string
+ setName(string) + getToken(): string
+ setProfilePicture(URI) + getName(): string
+ setAge(int) + getId(): int
} }
AccountUser ..|> User
class Member { class Member {
- userId: int - userId: int
- teamId: int
+ __construct(role : MemberRole) + __construct(role : MemberRole)
+ getUserId(): int + getUserId(): int
+ getTeamId(): int
+ getRole(): MemberRole + getRole(): MemberRole
} }
@ -50,20 +41,27 @@ enum MemberRole {
COACH COACH
} }
class Team {
class TeamInfo {
- creationDate: int
- name: string - name: string
- picture: Url - picture: string
+ __construct(name : string, picture : string, mainColor : Color, secondColor : Color,members : array)
+ getName(): string + getName(): string
+ getPicture(): Url + getPicture(): string
+ getMainColor(): Color + getMainColor(): Color
+ getSecondColor(): Color + getSecondColor(): Color
+ listMembers(): array<Member>
} }
Team --> "- mainColor" Color TeamInfo --> "- mainColor" Color
Team --> "- secondColor" Color TeamInfo --> "- secondaryColor" Color
class Team {
getInfo(): TeamInfo
listMembers(): Member[]
}
Team --> "- info" TeamInfo
Team --> "- members *" Member Team --> "- members *" Member
class Color { class Color {
@ -72,33 +70,4 @@ class Color {
+ getValue(): int + getValue(): int
} }
class AuthController{
+ __construct(model : AuthModel)
+ displayRegister() : HttpResponse
+ displayBadFields(viewName : string, fails : array) : HttpResponse
+ confirmRegister(request : array) : HttpResponse
+ displayLogin() : HttpResponse
+ confirmLogin() : HttpResponse
}
AuthController --> "- model" AuthModel
class AuthModel{
+ __construct(gateway : AuthGateway)
+ register(username : string, password : string, confirmPassword : string, email : string): array
+ getAccount(email : string):array
+ login(email : string, password : string)
}
AuthModel --> "- gateway" AuthGateway
class AuthGateway{
-con : Connection
+ __construct (con : Connection)
+ mailExist(email : string) : bool
+ insertAccount(username : string, hash : string, email : string)
+ getHash(email : string):string
+ getAccount (email : string): array
}
@enduml @enduml

@ -0,0 +1,28 @@
@startuml
class AuthController {
+ displayRegister() : HttpResponse
+ displayBadFields(viewName : string, fails : array) : HttpResponse
+ confirmRegister(request : array) : HttpResponse
+ displayLogin() : HttpResponse
+ confirmLogin() : HttpResponse
}
AuthController --> "- model" AuthModel
class AuthModel {
+ register(username : string, password : string, confirmPassword : string, email : string): array
+ getAccount(email : string):array
+ login(email : string, password : string)
}
AuthModel --> "- gateway" AuthGateway
class AuthGateway {
-con : Connection
+ mailExists(email : string) : bool
+ insertAccount(username : string, hash : string, email : string)
+ getUserHash(email : string):string
+ getAccount (email : string): array
}
@enduml

@ -58,12 +58,6 @@ class TeamController{
TeamController *--"- model" TeamModel TeamController *--"- model" TeamModel
class Connexion{ class Connexion { }
- pdo : PDO
--
+ __constructor(pdo : PDO)
+ exec(query : string, args : array)
+ fetch(query string, args array): array
}
@enduml @enduml

@ -1,8 +0,0 @@
@startuml
class FrontController {
- router : AltoRouter
}
@enduml

@ -50,9 +50,11 @@ class Validation {
} }
class Validators { class Validators {
---
+ <u>nonEmpty(): Validator + <u>nonEmpty(): Validator
+ <u>shorterThan(limit: int): Validator + <u>shorterThan(limit: int): Validator
+ <u>userString(maxLen: int): Validator + <u>userString(maxLen: int): Validator
...
} }

@ -13,16 +13,22 @@ use IQBall\App\Controller\EditorController;
use IQBall\App\Controller\TeamController; use IQBall\App\Controller\TeamController;
use IQBall\App\Controller\UserController; use IQBall\App\Controller\UserController;
use IQBall\App\Controller\VisualizerController; use IQBall\App\Controller\VisualizerController;
use IQBall\App\ViewHttpResponse;
use IQBall\Core\Action; use IQBall\Core\Action;
use IQBall\Core\Connection; use IQBall\Core\Connection;
use IQBall\Core\Gateway\AccountGateway; use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Gateway\MemberGateway;
use IQBall\Core\Gateway\TacticInfoGateway; use IQBall\Core\Gateway\TacticInfoGateway;
use IQBall\Core\Gateway\TeamGateway; use IQBall\Core\Gateway\TeamGateway;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\AuthModel;
use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TacticModel;
use IQBall\Core\Model\TeamModel; use IQBall\Core\Model\TeamModel;
use IQBall\Core\Session\MutableSessionHandle;
use IQBall\Core\Session\PhpSessionHandle; use IQBall\Core\Session\PhpSessionHandle;
use IQBall\Core\Session\SessionHandle; use IQBall\Core\Session\SessionHandle;
use IQBall\Core\Validation\ValidationFail;
function getConnection(): Connection { function getConnection(): Connection {
return new Connection(get_database()); return new Connection(get_database());
@ -41,7 +47,8 @@ function getEditorController(): EditorController {
} }
function getTeamController(): TeamController { function getTeamController(): TeamController {
return new TeamController(new TeamModel(new TeamGateway(getConnection()))); $con = getConnection();
return new TeamController(new TeamModel(new TeamGateway($con), new MemberGateway($con), new AccountGateway($con)));
} }
function getAuthController(): AuthController { function getAuthController(): AuthController {
@ -57,8 +64,8 @@ function getRoutes(): AltoRouter {
//authentication //authentication
$ar->map("GET", "/login", Action::noAuth(fn() => getAuthController()->displayLogin())); $ar->map("GET", "/login", Action::noAuth(fn() => getAuthController()->displayLogin()));
$ar->map("GET", "/register", Action::noAuth(fn() => getAuthController()->displayRegister())); $ar->map("GET", "/register", Action::noAuth(fn() => getAuthController()->displayRegister()));
$ar->map("POST", "/login", Action::noAuth(fn(SessionHandle $s) => getAuthController()->confirmLogin($_POST, $s))); $ar->map("POST", "/login", Action::noAuth(fn(SessionHandle $s) => getAuthController()->login($_POST, $s)));
$ar->map("POST", "/register", Action::noAuth(fn(SessionHandle $s) => getAuthController()->confirmRegister($_POST, $s))); $ar->map("POST", "/register", Action::noAuth(fn(SessionHandle $s) => getAuthController()->register($_POST, $s)));
//user-related //user-related
$ar->map("GET", "/home", Action::auth(fn(SessionHandle $s) => getUserController()->home($s))); $ar->map("GET", "/home", Action::auth(fn(SessionHandle $s) => getUserController()->home($s)));
@ -66,8 +73,8 @@ function getRoutes(): AltoRouter {
$ar->map("GET", "/settings", Action::auth(fn(SessionHandle $s) => getUserController()->settings($s))); $ar->map("GET", "/settings", Action::auth(fn(SessionHandle $s) => getUserController()->settings($s)));
//tactic-related //tactic-related
$ar->map("GET", "/tactic/[i:id]/view", Action::auth(fn(int $id, SessionHandle $s) => getVisualizerController()->visualize($id, $s))); $ar->map("GET", "/tactic/[i:id]/view", Action::auth(fn(int $id, SessionHandle $s) => getVisualizerController()->openVisualizer($id, $s)));
$ar->map("GET", "/tactic/[i:id]/edit", Action::auth(fn(int $id, SessionHandle $s) => getEditorController()->edit($id, $s))); $ar->map("GET", "/tactic/[i:id]/edit", Action::auth(fn(int $id, SessionHandle $s) => getEditorController()->openEditor($id, $s)));
$ar->map("GET", "/tactic/new", Action::auth(fn(SessionHandle $s) => getEditorController()->createNew($s))); $ar->map("GET", "/tactic/new", Action::auth(fn(SessionHandle $s) => getEditorController()->createNew($s)));
//team-related //team-related
@ -84,8 +91,19 @@ function getRoutes(): AltoRouter {
return $ar; return $ar;
} }
function runMatch($match, MutableSessionHandle $session): HttpResponse {
global $basePath;
if (!$match) {
return ViewHttpResponse::twig("error.html.twig", [
'failures' => [ValidationFail::notFound("Could not find page {$_SERVER['REQUEST_URI']}.")],
], HttpCodes::NOT_FOUND);
}
return App::runAction($basePath . '/login', $match['target'], $match['params'], $session);
}
//this is a global variable //this is a global variable
$basePath = get_public_path(); $basePath = get_public_path();
App::render(App::runMatch(getRoutes()->match(), PhpSessionHandle::init())); App::render(runMatch(getRoutes()->match(), PhpSessionHandle::init()), "../src/App/Views/");

@ -30,15 +30,15 @@ CREATE TABLE Team
id integer PRIMARY KEY AUTOINCREMENT, id integer PRIMARY KEY AUTOINCREMENT,
name varchar, name varchar,
picture varchar, picture varchar,
mainColor varchar, main_color varchar,
secondColor varchar second_color varchar
); );
CREATE TABLE Member( CREATE TABLE Member(
idTeam integer, id_team integer,
idMember integer, id_user integer,
role char(1) CHECK (role IN ('C', 'P')), role char(1) CHECK (role IN ('Coach', 'Player')),
FOREIGN KEY (idTeam) REFERENCES Team (id), FOREIGN KEY (id_team) REFERENCES Team (id),
FOREIGN KEY (idMember) REFERENCES User (id) FOREIGN KEY (id_user) REFERENCES User (id)
); );

@ -2,6 +2,7 @@
namespace IQBall\Api\Controller; namespace IQBall\Api\Controller;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Route\Control; use IQBall\Core\Route\Control;
use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
@ -20,16 +21,20 @@ class APIAuthController {
} }
/**
* From given email address and password, authenticate the user and respond with its authorization token.
* @return HttpResponse
*/
public function authorize(): HttpResponse { public function authorize(): HttpResponse {
return Control::runChecked([ return Control::runChecked([
"email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)], "email" => [Validators::email(), Validators::lenBetween(5, 256)],
"password" => [Validators::lenBetween(6, 256)], "password" => [Validators::lenBetween(6, 256)],
], 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);
if (!empty($failures)) { if (!empty($failures)) {
return new JsonHttpResponse($failures); return new JsonHttpResponse($failures, HttpCodes::UNAUTHORIZED);
} }
return new JsonHttpResponse(["authorization" => $account->getToken()]); return new JsonHttpResponse(["authorization" => $account->getToken()]);

@ -24,6 +24,12 @@ class APITacticController {
$this->model = $model; $this->model = $model;
} }
/**
* update name of tactic, specified by tactic identifier, given in url.
* @param int $tactic_id
* @param Account $account
* @return HttpResponse
*/
public function updateName(int $tactic_id, Account $account): HttpResponse { public function updateName(int $tactic_id, Account $account): HttpResponse {
return Control::runChecked([ return Control::runChecked([
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()],

@ -3,11 +3,9 @@
namespace IQBall\App; namespace IQBall\App;
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\Session\MutableSessionHandle; use IQBall\Core\Session\MutableSessionHandle;
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;
@ -15,7 +13,16 @@ use Twig\Error\SyntaxError;
use Twig\Loader\FilesystemLoader; use Twig\Loader\FilesystemLoader;
class App { class App {
public static function render(HttpResponse $response): void { /**
* renders (prints out) given HttpResponse to the client
* @param HttpResponse $response
* @param string $twigViewsFolder
* @return void
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public static function render(HttpResponse $response, string $twigViewsFolder): void {
http_response_code($response->getCode()); http_response_code($response->getCode());
foreach ($response->getHeaders() as $header => $value) { foreach ($response->getHeaders() as $header => $value) {
@ -23,14 +30,23 @@ class App {
} }
if ($response instanceof ViewHttpResponse) { if ($response instanceof ViewHttpResponse) {
self::renderView($response); self::renderView($response, $twigViewsFolder);
} elseif ($response instanceof JsonHttpResponse) { } elseif ($response instanceof JsonHttpResponse) {
header('Content-type: application/json'); header('Content-type: application/json');
echo $response->getJson(); echo $response->getJson();
} }
} }
private static function renderView(ViewHttpResponse $response): void { /**
* renders (prints out) given ViewHttpResponse to the client
* @param ViewHttpResponse $response
* @param string $twigViewsFolder
* @return void
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
private static function renderView(ViewHttpResponse $response, string $twigViewsFolder): void {
$file = $response->getFile(); $file = $response->getFile();
$args = $response->getArguments(); $args = $response->getArguments();
@ -40,8 +56,8 @@ class App {
break; break;
case ViewHttpResponse::TWIG_VIEW: case ViewHttpResponse::TWIG_VIEW:
try { try {
$loader = new FilesystemLoader('../src/App/Views/'); $fl = new FilesystemLoader($twigViewsFolder);
$twig = new Environment($loader); $twig = new Environment($fl);
$twig->display($file, $args); $twig->display($file, $args);
} catch (RuntimeError | SyntaxError | LoaderError $e) { } catch (RuntimeError | SyntaxError | LoaderError $e) {
http_response_code(500); http_response_code(500);
@ -53,38 +69,25 @@ class App {
} }
/** /**
* run a user action, and return the generated response
* @param string $authRoute the route towards an authentication page to response with a redirection
* if the run action requires auth but session does not contain a logged-in account.
* @param Action<MutableSessionHandle> $action * @param Action<MutableSessionHandle> $action
* @param mixed[] $params * @param mixed[] $params
* @param MutableSessionHandle $session * @param MutableSessionHandle $session
* @return HttpResponse * @return HttpResponse
*/ */
private static function runAction(Action $action, array $params, MutableSessionHandle $session): HttpResponse { public static function runAction(string $authRoute, Action $action, array $params, MutableSessionHandle $session): HttpResponse {
global $basePath;
if ($action->isAuthRequired()) { if ($action->isAuthRequired()) {
$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($basePath . "/login"); return HttpResponse::redirect($authRoute);
} }
} }
return $action->run($params, $session); return $action->run($params, $session);
} }
/**
* @param array<string, mixed>|false $match
* @param MutableSessionHandle $session
* @return HttpResponse
*/
public static function runMatch($match, MutableSessionHandle $session): HttpResponse {
if (!$match) {
return ViewHttpResponse::twig("error.html.twig", [
'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")],
], HttpCodes::NOT_FOUND);
}
return self::runAction($match['target'], $match['params'], $session);
}
} }

@ -25,33 +25,25 @@ class AuthController {
} }
/** /**
* @param string $viewName * registers given account
* @param ValidationFail[] $fails
* @return HttpResponse
*/
private function displayBadFields(string $viewName, array $fails): HttpResponse {
return ViewHttpResponse::twig($viewName, ['fails' => $fails]);
}
/**
* @param mixed[] $request * @param mixed[] $request
* @param MutableSessionHandle $session * @param MutableSessionHandle $session
* @return HttpResponse * @return HttpResponse
*/ */
public function confirmRegister(array $request, MutableSessionHandle $session): HttpResponse { public function register(array $request, MutableSessionHandle $session): HttpResponse {
$fails = []; $fails = [];
$request = HttpRequest::from($request, $fails, [ $request = HttpRequest::from($request, $fails, [
"username" => [Validators::name(), Validators::lenBetween(2, 32)], "username" => [Validators::name(), Validators::lenBetween(2, 32)],
"password" => [Validators::lenBetween(6, 256)], "password" => [Validators::lenBetween(6, 256)],
"confirmpassword" => [Validators::lenBetween(6, 256)], "confirmpassword" => [Validators::lenBetween(6, 256)],
"email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/", "invalide"),Validators::lenBetween(5, 256)], "email" => [Validators::email(), Validators::lenBetween(5, 256)],
]); ]);
if (!empty($fails)) { if (!empty($fails)) {
return $this->displayBadFields("display_register.html.twig", $fails); return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]);
} }
$account = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email'], $fails); $account = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email'], $fails);
if (!empty($fails)) { if (!empty($fails)) {
return $this->displayBadFields("display_register.html.twig", $fails); return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]);
} }
$session->setAccount($account); $session->setAccount($account);
@ -66,22 +58,24 @@ class AuthController {
} }
/** /**
* logins given account credentials
* @param mixed[] $request * @param mixed[] $request
* @param MutableSessionHandle $session
* @return HttpResponse * @return HttpResponse
*/ */
public function confirmLogin(array $request, MutableSessionHandle $session): HttpResponse { public function login(array $request, MutableSessionHandle $session): HttpResponse {
$fails = []; $fails = [];
$request = HttpRequest::from($request, $fails, [ $request = HttpRequest::from($request, $fails, [
"password" => [Validators::lenBetween(6, 256)], "password" => [Validators::lenBetween(6, 256)],
"email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/", "invalide"),Validators::lenBetween(5, 256)], "email" => [Validators::email(), Validators::lenBetween(5, 256)],
]); ]);
if (!empty($fails)) { if (!empty($fails)) {
return $this->displayBadFields("display_login.html.twig", $fails); return ViewHttpResponse::twig("display_login.html.twig", ['fails' => $fails]);
} }
$account = $this->model->login($request['email'], $request['password'], $fails); $account = $this->model->login($request['email'], $request['password'], $fails);
if (!empty($fails)) { if (!empty($fails)) {
return $this->displayBadFields("display_login.html.twig", $fails); return ViewHttpResponse::twig("display_login.html.twig", ['fails' => $fails]);
} }
$session->setAccount($account); $session->setAccount($account);

@ -17,22 +17,31 @@ class EditorController {
$this->model = $model; $this->model = $model;
} }
private function openEditor(TacticInfo $tactic): HttpResponse { /**
* @param TacticInfo $tactic
* @return ViewHttpResponse the editor view for given tactic
*/
private function openEditorFor(TacticInfo $tactic): ViewHttpResponse {
return ViewHttpResponse::react("views/Editor.tsx", ["name" => $tactic->getName(), "id" => $tactic->getId()]); return ViewHttpResponse::react("views/Editor.tsx", ["name" => $tactic->getName(), "id" => $tactic->getId()]);
} }
public function createNew(SessionHandle $session): HttpResponse { /**
* creates a new empty tactic, with default name
* @param SessionHandle $session
* @return ViewHttpResponse the editor view
*/
public function createNew(SessionHandle $session): ViewHttpResponse {
$tactic = $this->model->makeNewDefault($session->getAccount()->getId()); $tactic = $this->model->makeNewDefault($session->getAccount()->getId());
return $this->openEditor($tactic); return $this->openEditorFor($tactic);
} }
/** /**
* returns an editor view for a given tactic * returns an editor view for a given tactic
* @param int $id the targeted tactic identifier * @param int $id the targeted tactic identifier
* @param SessionHandle $session * @param SessionHandle $session
* @return HttpResponse * @return ViewHttpResponse
*/ */
public function edit(int $id, SessionHandle $session): HttpResponse { public function openEditor(int $id, SessionHandle $session): ViewHttpResponse {
$tactic = $this->model->get($id); $tactic = $this->model->get($id);
$failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId()); $failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId());
@ -41,7 +50,7 @@ class EditorController {
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);
} }
return $this->openEditor($tactic); return $this->openEditorFor($tactic);
} }

@ -20,20 +20,34 @@ class TeamController {
$this->model = $model; $this->model = $model;
} }
public function displayCreateTeam(SessionHandle $session): HttpResponse { /**
* @param SessionHandle $session
* @return ViewHttpResponse the team creation panel
*/
public function displayCreateTeam(SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("insert_team.html.twig", []); return ViewHttpResponse::twig("insert_team.html.twig", []);
} }
public function displayAddMember(SessionHandle $session): HttpResponse { /**
* @param SessionHandle $session
* @return ViewHttpResponse the team panel to add a member
*/
public function displayAddMember(SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("add_member.html.twig", []); return ViewHttpResponse::twig("add_member.html.twig", []);
} }
public function displayDeleteMember(SessionHandle $session): HttpResponse {
/**
* @param SessionHandle $session
* @return ViewHttpResponse the team panel to delete a member
*/
public function displayDeleteMember(SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("delete_member.html.twig", []); return ViewHttpResponse::twig("delete_member.html.twig", []);
} }
/** /**
* create a new team from given request name, mainColor, secondColor and picture url
* @param array<string, mixed> $request * @param array<string, mixed> $request
* @param SessionHandle $session * @param SessionHandle $session
* @return HttpResponse * @return HttpResponse
@ -42,8 +56,8 @@ class TeamController {
$failures = []; $failures = [];
$request = HttpRequest::from($request, $failures, [ $request = HttpRequest::from($request, $failures, [
"name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()],
"mainColor" => [Validators::regex('/#(?:[0-9a-fA-F]{6})/')], "main_color" => [Validators::hexColor()],
"secondColor" => [Validators::regex('/#(?:[0-9a-fA-F]{6})/')], "second_color" => [Validators::hexColor()],
"picture" => [Validators::isURL()], "picture" => [Validators::isURL()],
]); ]);
if (!empty($failures)) { if (!empty($failures)) {
@ -55,15 +69,22 @@ class TeamController {
} }
return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]); return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]);
} }
return $this->displayTeam($this->model->createTeam($request['name'], $request['picture'], $request['mainColor'], $request['secondColor']), $session); $teamId = $this->model->createTeam($request['name'], $request['picture'], $request['main_color'], $request['second_color']);
return $this->displayTeam($teamId, $session);
} }
public function displayListTeamByName(SessionHandle $session): HttpResponse { /**
* @param SessionHandle $session
* @return ViewHttpResponse the panel to search a team by its name
*/
public function displayListTeamByName(SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("list_team_by_name.html.twig", []); return ViewHttpResponse::twig("list_team_by_name.html.twig", []);
} }
/** /**
* @param array<string , mixed> $request * returns a view that contains all the teams description whose name matches the given name needle.
* @param array<string, mixed> $request
* @param SessionHandle $session
* @return HttpResponse * @return HttpResponse
*/ */
public function listTeamByName(array $request, SessionHandle $session): HttpResponse { public function listTeamByName(array $request, SessionHandle $session): HttpResponse {
@ -77,21 +98,27 @@ class TeamController {
return ViewHttpResponse::twig('list_team_by_name.html.twig', ['bad_field' => $badField]); return ViewHttpResponse::twig('list_team_by_name.html.twig', ['bad_field' => $badField]);
} }
$results = $this->model->listByName($request['name']); $teams = $this->model->listByName($request['name']);
if (empty($results)) { if (empty($teams)) {
return ViewHttpResponse::twig('display_teams.html.twig', []); return ViewHttpResponse::twig('display_teams.html.twig', []);
} }
return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $results]); return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $teams]);
} }
public function displayTeam(int $id, SessionHandle $session): HttpResponse { /**
$result = $this->model->displayTeam($id); * @param int $id
* @param SessionHandle $session
* @return ViewHttpResponse a view that displays given team information
*/
public function displayTeam(int $id, SessionHandle $session): ViewHttpResponse {
$result = $this->model->getTeam($id);
return ViewHttpResponse::twig('display_team.html.twig', ['team' => $result]); return ViewHttpResponse::twig('display_team.html.twig', ['team' => $result]);
} }
/** /**
* add a member to a team
* @param array<string, mixed> $request * @param array<string, mixed> $request
* @param SessionHandle $session * @param SessionHandle $session
* @return HttpResponse * @return HttpResponse
@ -101,13 +128,16 @@ class TeamController {
$request = HttpRequest::from($request, $errors, [ $request = HttpRequest::from($request, $errors, [
"team" => [Validators::isInteger()], "team" => [Validators::isInteger()],
"mail" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)], "email" => [Validators::email(), Validators::lenBetween(5, 256)],
]); ]);
return $this->displayTeam($this->model->addMember($request['mail'], intval($request['team']), $request['role']), $session); $teamId = intval($request['team']);
$this->model->addMember($request['email'], $teamId, $request['role']);
return $this->displayTeam($teamId, $session);
} }
/** /**
* remove a member from a team
* @param array<string, mixed> $request * @param array<string, mixed> $request
* @param SessionHandle $session * @param SessionHandle $session
* @return HttpResponse * @return HttpResponse
@ -117,9 +147,9 @@ class TeamController {
$request = HttpRequest::from($request, $errors, [ $request = HttpRequest::from($request, $errors, [
"team" => [Validators::isInteger()], "team" => [Validators::isInteger()],
"mail" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)], "email" => [Validators::email(), Validators::lenBetween(5, 256)],
]); ]);
return $this->displayTeam($this->model->deleteMember($request['mail'], intval($request['team'])), $session); return $this->displayTeam($this->model->deleteMember($request['email'], intval($request['team'])), $session);
} }
} }

@ -19,18 +19,18 @@ class UserController {
/** /**
* @param SessionHandle $session * @param SessionHandle $session
* @return HttpResponse the home page * @return ViewHttpResponse the home page view
*/ */
public function home(SessionHandle $session): HttpResponse { public function home(SessionHandle $session): ViewHttpResponse {
//TODO use session's account to get the last 5 tactics if the logged-in account //TODO use session's account to get the last 5 tactics of the logged-in account
$listTactic = $this->tactics->getLast(5); $listTactic = $this->tactics->getLast(5);
return ViewHttpResponse::twig("home.twig", ["recentTactic" => $listTactic]); return ViewHttpResponse::twig("home.twig", ["recentTactic" => $listTactic]);
} }
/** /**
* @return HttpResponse account settings page * @return ViewHttpResponse account settings page
*/ */
public function settings(SessionHandle $session): HttpResponse { public function settings(SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("account_settings.twig", []); return ViewHttpResponse::twig("account_settings.twig", []);
} }

@ -19,7 +19,13 @@ class VisualizerController {
$this->tacticModel = $tacticModel; $this->tacticModel = $tacticModel;
} }
public function visualize(int $id, SessionHandle $session): HttpResponse { /**
* opens a visualisation page for the tactic specified by its identifier in the url.
* @param int $id
* @param SessionHandle $session
* @return HttpResponse
*/
public function openVisualizer(int $id, SessionHandle $session): HttpResponse {
$tactic = $this->tacticModel->get($id); $tactic = $this->tacticModel->get($id);
$failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId()); $failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId());

@ -20,17 +20,13 @@
height:50px; height:50px;
} }
#mainColor{ #main_color {
background-color: {{ team.mainColor.getValue() }}; border: solid;
{% if team.mainColor.getValue() == "#ffffff" %} background-color: {{ team.getInfo().getMainColor().getValue() }};
border-color: #666666;
{% endif %}
} }
#secondColor{ #second_color{
background-color: {{ team.secondColor.getValue() }}; background-color: {{ team.getInfo().getSecondColor().getValue() }};
{% if team.secondColor.getValue() == "#ffffff" %} border: solid;
border-color: #666666;
{% endif %}
} }
.container{ .container{
@ -66,12 +62,12 @@
<div class="team container"> <div class="team container">
<div> <div>
<h1>{{ team.name }}</h1> <h1>{{ team.getInfo().getName() }}</h1>
<img src="{{ team.picture }}" alt="Logo d'équipe" class="logo"> <img src="{{ team.getInfo().getPicture() }}" alt="Logo d'équipe" class="logo">
</div> </div>
<div> <div>
<div class="color"><p>Couleur principale : </p><div class="square" id="mainColor"></div> </div> <div class="color"><p>Couleur principale : </p><div class="square" id="main_color"></div> </div>
<div class="color"><p>Couleur secondaire : </p><div class="square" id="secondColor"></div></div> <div class="color"><p>Couleur secondaire : </p><div class="square" id="second_color"></div></div>
</div> </div>
{% for m in team.listMembers() %} {% for m in team.listMembers() %}

@ -70,10 +70,10 @@
<input type="text" id="name" name="name" required> <input type="text" id="name" name="name" required>
<label for= "picture">Logo:</label> <label for= "picture">Logo:</label>
<input type="text" id="picture" name="picture" required > <input type="text" id="picture" name="picture" required >
<label for="mainColor">Couleur principale</label> <label for="main_color">Couleur principale</label>
<input type="color" id="mainColor" name="mainColor" required> <input type="color" id="main_color" name="main_color" required>
<label for="secondColor">Couleur secondaire</label> <label for="second_color">Couleur secondaire</label>
<input type="color" id="secondColor" name="secondColor" required> <input type="color" id="second_color" name="second_color" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="submit" value="Confirmer"> <input type="submit" value="Confirmer">

@ -5,6 +5,7 @@ namespace IQBall\Core;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
/** /**
* Represent an action.
* @template S session * @template S session
*/ */
class Action { class Action {
@ -28,6 +29,7 @@ class Action {
} }
/** /**
* Runs an action
* @param mixed[] $params * @param mixed[] $params
* @param S $session * @param S $session
* @return HttpResponse * @return HttpResponse

@ -1,52 +0,0 @@
<?php
namespace IQBall\Core\Data;
use http\Url;
/**
* This class implements the User and
*/
class AccountUser implements User {
private string $name;
private Url $profilePicture;
private int $age;
/**
* @param string $name
* @param Url $profilePicture
* @param int $age
*/
public function __construct(string $name, Url $profilePicture, int $age) {
$this->name = $name;
$this->profilePicture = $profilePicture;
$this->age = $age;
}
public function getName(): string {
return $this->name;
}
public function getProfilePicture(): Url {
return $this->profilePicture;
}
public function getAge(): int {
return $this->age;
}
public function setName(string $name): void {
$this->name = $name;
}
public function setProfilePicture(Url $profilePicture): void {
$this->profilePicture = $profilePicture;
}
public function setAge(int $age): void {
$this->age = $age;
}
}

@ -7,7 +7,7 @@ use InvalidArgumentException;
/** /**
* Enumeration class workaround * Enumeration class workaround
* As there is no enumerations in php 7.4, this class * As there is no enumerations in php 7.4, this class
* encapsulates an integer value and use it as an enumeration discriminant * encapsulates an integer value and use it as a variant discriminant
*/ */
final class MemberRole { final class MemberRole {
private const ROLE_PLAYER = 0; private const ROLE_PLAYER = 0;
@ -32,6 +32,27 @@ final class MemberRole {
return new MemberRole(MemberRole::ROLE_COACH); return new MemberRole(MemberRole::ROLE_COACH);
} }
public function name(): string {
switch ($this->value) {
case self::ROLE_COACH:
return "Coach";
case self::ROLE_PLAYER:
return "Player";
}
die("unreachable");
}
public static function fromName(string $name): ?MemberRole {
switch ($name) {
case "Coach":
return MemberRole::coach();
case "Player":
return MemberRole::player();
default:
return null;
}
}
private function isValid(int $val): bool { private function isValid(int $val): bool {
return ($val <= self::MAX and $val >= self::MIN); return ($val <= self::MAX and $val >= self::MIN);
} }

@ -2,24 +2,23 @@
namespace IQBall\Core\Data; namespace IQBall\Core\Data;
class TacticInfo implements \JsonSerializable { class TacticInfo {
private int $id; private int $id;
private string $name; private string $name;
private int $creation_date; private int $creationDate;
private int $ownerId; private int $ownerId;
/** /**
* @param int $id * @param int $id
* @param string $name * @param string $name
* @param int $creation_date * @param int $creationDate
* @param int $ownerId * @param int $ownerId
*/ */
public function __construct(int $id, string $name, int $creation_date, int $ownerId) { public function __construct(int $id, string $name, int $creationDate, int $ownerId) {
$this->id = $id; $this->id = $id;
$this->name = $name; $this->name = $name;
$this->ownerId = $ownerId; $this->ownerId = $ownerId;
$this->creation_date = $creation_date; $this->creationDate = $creationDate;
} }
public function getId(): int { public function getId(): int {
@ -38,13 +37,7 @@ class TacticInfo implements \JsonSerializable {
} }
public function getCreationTimestamp(): int { public function getCreationTimestamp(): int {
return $this->creation_date; return $this->creationDate;
} }
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array {
return get_object_vars($this);
}
} }

@ -3,11 +3,7 @@
namespace IQBall\Core\Data; namespace IQBall\Core\Data;
class Team { class Team {
private int $id; private TeamInfo $info;
private string $name;
private string $picture;
private Color $mainColor;
private Color $secondColor;
/** /**
* @var Member[] maps users with their role * @var Member[] maps users with their role
@ -15,54 +11,16 @@ class Team {
private array $members; private array $members;
/** /**
* @param string $name * @param TeamInfo $info
* @param string $picture
* @param Color $mainColor
* @param Color $secondColor
* @param Member[] $members * @param Member[] $members
*/ */
public function __construct(int $id, string $name, string $picture, Color $mainColor, Color $secondColor, array $members = []) { public function __construct(TeamInfo $info, array $members = []) {
$this->id = $id; $this->info = $info;
$this->name = $name;
$this->picture = $picture;
$this->mainColor = $mainColor;
$this->secondColor = $secondColor;
$this->members = $members; $this->members = $members;
} }
/** public function getInfo(): TeamInfo {
* @return int return $this->info;
*/
public function getId(): int {
return $this->id;
}
/**
* @return string
*/
public function getName(): string {
return $this->name;
}
/**
* @return string
*/
public function getPicture(): string {
return $this->picture;
}
/**
* @return Color
*/
public function getMainColor(): Color {
return $this->mainColor;
}
/**
* @return Color
*/
public function getSecondColor(): Color {
return $this->secondColor;
} }
/** /**
@ -71,9 +29,4 @@ class Team {
public function listMembers(): array { public function listMembers(): array {
return $this->members; return $this->members;
} }
public function addMember(Member $m): void {
$this->members[] = $m;
}
} }

@ -0,0 +1,49 @@
<?php
namespace IQBall\Core\Data;
class TeamInfo {
private int $id;
private string $name;
private string $picture;
private Color $mainColor;
private Color $secondColor;
/**
* @param int $id
* @param string $name
* @param string $picture
* @param Color $mainColor
* @param Color $secondColor
*/
public function __construct(int $id, string $name, string $picture, Color $mainColor, Color $secondColor) {
$this->id = $id;
$this->name = $name;
$this->picture = $picture;
$this->mainColor = $mainColor;
$this->secondColor = $secondColor;
}
public function getId(): int {
return $this->id;
}
public function getName(): string {
return $this->name;
}
public function getPicture(): string {
return $this->picture;
}
public function getMainColor(): Color {
return $this->mainColor;
}
public function getSecondColor(): Color {
return $this->secondColor;
}
}

@ -1,26 +0,0 @@
<?php
namespace IQBall\Core\Data;
use http\Url;
/**
* Public information about a user
*/
interface User {
/**
* @return string the user's name
*/
public function getName(): string;
/**
* @return Url The user's profile picture image URL
*/
public function getProfilePicture(): Url;
/**
* @return int The user's age
*/
public function getAge(): int;
}

@ -35,7 +35,10 @@ class AccountGateway {
return $this->con->fetch("SELECT * FROM Account WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]])[0] ?? null; return $this->con->fetch("SELECT * FROM Account WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]])[0] ?? null;
} }
/**
* @param string $email
* @return string|null the hashed user's password, or null if the given mail does not exist
*/
public function getHash(string $email): ?string { public function getHash(string $email): ?string {
$results = $this->getRowsFromMail($email); $results = $this->getRowsFromMail($email);
if ($results == null) { if ($results == null) {
@ -44,6 +47,10 @@ class AccountGateway {
return $results['hash']; return $results['hash'];
} }
/**
* @param string $email
* @return bool true if the given email exists in the database
*/
public function exists(string $email): bool { public function exists(string $email): bool {
return $this->getRowsFromMail($email) != null; return $this->getRowsFromMail($email) != null;
} }
@ -61,6 +68,10 @@ class AccountGateway {
return new Account($email, $acc["username"], $acc["token"], $acc["id"]); return new Account($email, $acc["username"], $acc["token"], $acc["id"]);
} }
/**
* @param string $token get an account from given token
* @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; $acc = $this->con->fetch("SELECT * FROM Account WHERE token = :token", [':token' => [$token, PDO::PARAM_STR]])[0] ?? null;
if (empty($acc)) { if (empty($acc)) {

@ -1,47 +0,0 @@
<?php
namespace IQBall\Core\Gateway;
use IQBall\Core\Connection;
use PDO;
class AuthGateway {
private Connection $con;
/**
* @param Connection $con
*/
public function __construct(Connection $con) {
$this->con = $con;
}
public function mailExist(string $email): bool {
return $this->getUserFields($email) != null;
}
public function insertAccount(string $username, string $hash, string $email): void {
$this->con->exec("INSERT INTO AccountUser(username, hash, email) VALUES (:username,:hash,:email)", [':username' => [$username, PDO::PARAM_STR],':hash' => [$hash, PDO::PARAM_STR],':email' => [$email, PDO::PARAM_STR]]);
}
public function getUserHash(string $email): string {
$results = $this->con->fetch("SELECT hash FROM AccountUser WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]);
return $results[0]['hash'];
}
/**
* @param string $email
* @return array<string,string>|null
*/
public function getUserFields(string $email): ?array {
$results = $this->con->fetch("SELECT username,email FROM AccountUser WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]);
$firstRow = $results[0] ?? null;
return $firstRow;
}
}

@ -0,0 +1,69 @@
<?php
namespace IQBall\Core\Gateway;
use IQBall\Core\Connection;
use IQBall\Core\Data\Member;
use IQBall\Core\Data\MemberRole;
use PDO;
class MemberGateway {
private Connection $con;
/**
* @param Connection $con
*/
public function __construct(Connection $con) {
$this->con = $con;
}
/**
* insert member to a team
* @param int $idTeam
* @param int $userId
* @param string $role
* @return void
*/
public function insert(int $idTeam, int $userId, string $role): void {
$this->con->exec(
"INSERT INTO Member(id_team, id_user, role) VALUES (:id_team, :id_user, :role)",
[
":id_team" => [$idTeam, PDO::PARAM_INT],
":id_user" => [$userId, PDO::PARAM_INT],
":role" => [$role, PDO::PARAM_STR],
]
);
}
/**
* @param int $teamId
* @return Member[]
*/
public function getMembersOfTeam(int $teamId): array {
$rows = $this->con->fetch(
"SELECT a.id,m.role,a.email,a.username 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($row['id_user'], $row['id_team'], MemberRole::fromName($row['role'])), $rows);
}
/**
* remove member from given team
* @param int $idTeam
* @param int $idMember
* @return void
*/
public function remove(int $idTeam, int $idMember): void {
$this->con->exec(
"DELETE FROM Member WHERE id_team = :id_team AND id_user = :id_user",
[
":id_team" => [$idTeam, PDO::PARAM_INT],
":id_user" => [$idMember, PDO::PARAM_INT],
]
);
}
}

@ -16,6 +16,11 @@ class TacticInfoGateway {
$this->con = $con; $this->con = $con;
} }
/**
* get tactic information from given identifier
* @param int $id
* @return TacticInfo|null
*/
public function get(int $id): ?TacticInfo { public function get(int $id): ?TacticInfo {
$res = $this->con->fetch( $res = $this->con->fetch(
"SELECT * FROM Tactic WHERE id = :id", "SELECT * FROM Tactic WHERE id = :id",
@ -49,6 +54,11 @@ class TacticInfoGateway {
return $res; return $res;
} }
/**
* @param string $name
* @param int $owner
* @return TacticInfo
*/
public function insert(string $name, int $owner): TacticInfo { public function insert(string $name, int $owner): TacticInfo {
$this->con->exec( $this->con->exec(
"INSERT INTO Tactic(name, owner) VALUES(:name, :owner)", "INSERT INTO Tactic(name, owner) VALUES(:name, :owner)",
@ -64,6 +74,12 @@ class TacticInfoGateway {
return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"]), $row["owner"]); return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"]), $row["owner"]);
} }
/**
* update name of given tactic identifier
* @param int $id
* @param string $name
* @return void
*/
public function updateName(int $id, string $name): void { public function updateName(int $id, string $name): void {
$this->con->exec( $this->con->exec(
"UPDATE Tactic SET name = :name WHERE id = :id", "UPDATE Tactic SET name = :name WHERE id = :id",

@ -3,6 +3,8 @@
namespace IQBall\Core\Gateway; namespace IQBall\Core\Gateway;
use IQBall\Core\Connection; use IQBall\Core\Connection;
use IQBall\Core\Data\Color;
use IQBall\Core\Data\TeamInfo;
use PDO; use PDO;
class TeamGateway { class TeamGateway {
@ -12,62 +14,65 @@ class TeamGateway {
$this->con = $con; $this->con = $con;
} }
public function insert(string $name, string $picture, string $mainColor, string $secondColor): void { /**
* @param string $name
* @param string $picture
* @param string $mainColor
* @param string $secondColor
* @return int the inserted team identifier
*/
public function insert(string $name, string $picture, string $mainColor, string $secondColor): int {
$this->con->exec( $this->con->exec(
"INSERT INTO Team(name, picture, mainColor, secondColor) VALUES (:teamName , :picture, :mainColor, :secondColor)", "INSERT INTO Team(name, picture, main_color, second_color) VALUES (:team_name , :picture, :main_color, :second_color)",
[ [
":teamName" => [$name, PDO::PARAM_STR], ":team_name" => [$name, PDO::PARAM_STR],
":picture" => [$picture, PDO::PARAM_STR], ":picture" => [$picture, PDO::PARAM_STR],
":mainColor" => [$mainColor, PDO::PARAM_STR], ":main_color" => [$mainColor, PDO::PARAM_STR],
":secondColor" => [$secondColor, PDO::PARAM_STR], ":second_color" => [$secondColor, PDO::PARAM_STR],
]
);
}
public function insertMember(int $idTeam, int $idMember, string $role): void {
$this->con->exec(
"INSERT INTO Member(idTeam, idMember, role) VALUES (:idTeam , :idMember, :role)",
[
":idTeam" => [$idTeam, PDO::PARAM_INT],
":idMember" => [$idMember, PDO::PARAM_INT],
":role" => [$role, PDO::PARAM_STR],
] ]
); );
return intval($this->con->lastInsertId());
} }
/** /**
* @param string $name * @param string $name
* @return array<string, mixed>[] * @return TeamInfo[]
*/ */
public function listByName(string $name): array { public function listByName(string $name): array {
return $this->con->fetch( $result = $this->con->fetch(
"SELECT id,name,picture,mainColor,secondColor FROM Team WHERE name LIKE '%' || :name || '%'", "SELECT * FROM Team WHERE name LIKE '%' || :name || '%'",
[ [
":name" => [$name, PDO::PARAM_STR], ":name" => [$name, PDO::PARAM_STR],
] ]
); );
return array_map(fn($row) => new TeamInfo($row['id'], $row['name'], $row['picture'], Color::from($row['main_color']), Color::from($row['second_color'])), $result);
} }
/** /**
* @param int $id * @param int $id
* @return array<string,mixed> * @return TeamInfo
*/ */
public function getTeamById(int $id): ?array { public function getTeamById(int $id): ?TeamInfo {
return $this->con->fetch( $row = $this->con->fetch(
"SELECT id,name,picture,mainColor,secondColor FROM Team WHERE id = :id", "SELECT * FROM Team WHERE id = :id",
[ [
":id" => [$id, PDO::PARAM_INT], ":id" => [$id, PDO::PARAM_INT],
] ]
)[0] ?? null; )[0] ?? null;
if ($row == null) {
return null;
}
return new TeamInfo($row['id'], $row['name'], $row['picture'], Color::from($row['main_color']), Color::from($row['second_color']));
} }
/** /**
* @param string $name * @param string $name
* @return int|null * @return int|null
*/ */
public function getIdTeamByName(string $name): ?int { public function getTeamIdByName(string $name): ?int {
return $this->con->fetch( return $this->con->fetch(
"SELECT id FROM Team WHERE name = :name", "SELECT id FROM Team WHERE name = :name",
[ [
@ -76,40 +81,5 @@ class TeamGateway {
)[0]['id'] ?? null; )[0]['id'] ?? null;
} }
/**
* @param int $id
* @return array<string,mixed>[]
*/
public function getMembersById(int $id): array {
return $this->con->fetch(
"SELECT a.id,m.role,a.email,a.username FROM Account a,Team t,Member m WHERE t.id = :id AND m.idTeam = t.id AND m.idMember = a.id",
[
":id" => [$id, PDO::PARAM_INT],
]
);
}
/**
* @param string $mail
* @return int|null
*/
public function getMemberIdByMail(string $mail): ?int {
return $this->con->fetch(
"SELECT id FROM Account WHERE email = :mail",
[
":mail" => [$mail, PDO::PARAM_STR],
]
)[0]['id'] ?? null;
}
public function deleteMember(int $idTeam, int $idMember): void {
$this->con->exec(
"DELETE FROM Member WHERE idTeam = :idTeam AND idMember = :idMember",
[
":idTeam" => [$idTeam, PDO::PARAM_INT],
":idMember" => [$idMember, PDO::PARAM_INT],
]
);
}
} }

@ -9,6 +9,7 @@ class HttpCodes {
public const OK = 200; public const OK = 200;
public const FOUND = 302; public const FOUND = 302;
public const BAD_REQUEST = 400; public const BAD_REQUEST = 400;
public const UNAUTHORIZED = 401;
public const FORBIDDEN = 403; public const FORBIDDEN = 403;

@ -29,10 +29,19 @@ class HttpResponse {
return $this->headers; return $this->headers;
} }
/**
* @param int $code
* @return HttpResponse
*/
public static function fromCode(int $code): HttpResponse { public static function fromCode(int $code): HttpResponse {
return new HttpResponse($code, []); return new HttpResponse($code, []);
} }
/**
* @param string $url the url to redirect
* @param int $code only HTTP 3XX codes are accepted.
* @return HttpResponse a response that will redirect client to given url
*/
public static function redirect(string $url, int $code = HttpCodes::FOUND): HttpResponse { public static function redirect(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");

@ -47,6 +47,10 @@ class AuthModel {
return new Account($email, $username, $token, $accountId); return new Account($email, $username, $token, $accountId);
} }
/**
* Generate a random base 64 string
* @return string
*/
private function generateToken(): string { private function generateToken(): string {
return base64_encode(random_bytes(64)); return base64_encode(random_bytes(64));
} }
@ -58,11 +62,11 @@ class AuthModel {
* @return Account|null the authenticated account or null if failures occurred * @return Account|null the authenticated account or null if failures occurred
*/ */
public function login(string $email, string $password, array &$failures): ?Account { public function login(string $email, string $password, array &$failures): ?Account {
if (!$this->gateway->exists($email)) { $hash = $this->gateway->getHash($email);
$failures[] = new FieldValidationFail("email", "Vous n'êtes pas enregistré."); if ($hash == null) {
$failures[] = new FieldValidationFail("email", "l'addresse email n'est pas connue.");
return null; return null;
} }
$hash = $this->gateway->getHash($email);
if (!password_verify($password, $hash)) { if (!password_verify($password, $hash)) {
$failures[] = new FieldValidationFail("password", "Mot de passe invalide."); $failures[] = new FieldValidationFail("password", "Mot de passe invalide.");

@ -19,12 +19,23 @@ class TacticModel {
$this->tactics = $tactics; $this->tactics = $tactics;
} }
/**
* creates a new empty tactic, with given name
* @param string $name
* @param int $ownerId
* @return TacticInfo
*/
public function makeNew(string $name, int $ownerId): TacticInfo { public function makeNew(string $name, int $ownerId): TacticInfo {
return $this->tactics->insert($name, $ownerId); return $this->tactics->insert($name, $ownerId);
} }
/**
* creates a new empty tactic, with a default name
* @param int $ownerId
* @return TacticInfo|null
*/
public function makeNewDefault(int $ownerId): ?TacticInfo { public function makeNewDefault(int $ownerId): ?TacticInfo {
return $this->tactics->insert(self::TACTIC_DEFAULT_NAME, $ownerId); return $this->makeNew(self::TACTIC_DEFAULT_NAME, $ownerId);
} }
/** /**

@ -2,65 +2,80 @@
namespace IQBall\Core\Model; namespace IQBall\Core\Model;
use IQBall\Core\Gateway\TeamGateway;
use IQBall\Core\Data\Team;
use IQBall\Core\Data\Member;
use IQBall\Core\Data\MemberRole;
use IQBall\Core\Data\Color; use IQBall\Core\Data\Color;
use IQBall\Core\Data\Team;
use IQBall\Core\Data\TeamInfo;
use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Gateway\MemberGateway;
use IQBall\Core\Gateway\TeamGateway;
class TeamModel { class TeamModel {
private TeamGateway $gateway; private AccountGateway $users;
private TeamGateway $teams;
private MemberGateway $members;
/** /**
* @param TeamGateway $gateway * @param TeamGateway $gateway
* @param MemberGateway $members
* @param AccountGateway $users
*/ */
public function __construct(TeamGateway $gateway) { public function __construct(TeamGateway $gateway, MemberGateway $members, AccountGateway $users) {
$this->gateway = $gateway; $this->teams = $gateway;
$this->members = $members;
$this->users = $users;
} }
/**
* @param string $name
* @param string $picture
* @param string $mainColor
* @param string $secondColor
* @return int
*/
public function createTeam(string $name, string $picture, string $mainColor, string $secondColor): int { public function createTeam(string $name, string $picture, string $mainColor, string $secondColor): int {
$this->gateway->insert($name, $picture, $mainColor, $secondColor); return $this->teams->insert($name, $picture, $mainColor, $secondColor);
return $this->gateway->getIdTeamByName($name);
} }
public function addMember(string $mail, int $teamId, string $role): int { /**
$id = $this->gateway->getMemberIdByMail($mail); * adds a member to a team
$this->gateway->insertMember($teamId, $id, $role); * @param string $mail
return $teamId; * @param int $teamId
* @param string $role
* @return void
*/
public function addMember(string $mail, int $teamId, string $role): void {
$userId = $this->users->getAccountFromMail($mail)->getId();
$this->members->insert($teamId, $userId, $role);
} }
/** /**
* @param string $name * @param string $name
* @return Team[] * @return TeamInfo[]
*/ */
public function listByName(string $name): array { public function listByName(string $name): array {
$teams = []; return $this->teams->listByName($name);
$results = $this->gateway->listByName($name);
foreach ($results as $row) {
$teams[] = new Team($row['id'], $row['name'], $row['picture'], Color::from($row['mainColor']), Color::from($row['secondColor']));
}
return $teams;
} }
public function displayTeam(int $id): Team { /**
$members = []; * @param int $id
$result = $this->gateway->getTeamById($id); * @return Team
$resultMembers = $this->gateway->getMembersById($id); */
foreach ($resultMembers as $row) { public function getTeam(int $id): Team {
var_dump($row['role']); $teamInfo = $this->teams->getTeamById($id);
if ($row['role'] == 'C') { $members = $this->members->getMembersOfTeam($id);
$role = MemberRole::coach(); return new Team($teamInfo, $members);
} else {
$role = MemberRole::player();
}
$members[] = new Member($row['id'], $id, $role);
}
return new Team(intval($result['id']), $result['name'], $result['picture'], Color::from($result['mainColor']), Color::from($result['secondColor']), $members);
} }
/**
* delete a member from given team identifier
* @param string $mail
* @param int $teamId
* @return int
*/
public function deleteMember(string $mail, int $teamId): int { public function deleteMember(string $mail, int $teamId): int {
$memberId = $this->gateway->getMemberIdByMail($mail); $userId = $this->users->getAccountFromMail($mail)->getId();
$this->gateway->deleteMember($teamId, $memberId); $this->members->remove($teamId, $userId);
return $teamId; return $teamId;
} }

@ -33,10 +33,18 @@ class ValidationFail implements JsonSerializable {
return ["error" => $this->kind, "message" => $this->message]; return ["error" => $this->kind, "message" => $this->message];
} }
/**
* @param string $message
* @return ValidationFail validation fail for unknown resource access
*/
public static function notFound(string $message): ValidationFail { public static function notFound(string $message): ValidationFail {
return new ValidationFail("Not found", $message); return new ValidationFail("Not found", $message);
} }
/**
* @param string $message
* @return ValidationFail validation fail for unauthorized accesses
*/
public static function unauthorized(string $message = "Unauthorized"): ValidationFail { public static function unauthorized(string $message = "Unauthorized"): ValidationFail {
return new ValidationFail("Unauthorized", $message); return new ValidationFail("Unauthorized", $message);
} }

@ -12,10 +12,18 @@ class Validators {
public static function regex(string $regex, ?string $msg = null): Validator { public static function regex(string $regex, ?string $msg = null): Validator {
return new SimpleFunctionValidator( return new SimpleFunctionValidator(
fn(string $str) => preg_match($regex, $str), fn(string $str) => preg_match($regex, $str),
fn(string $name) => [new FieldValidationFail($name, $msg == null ? "field does not validates pattern $regex" : $msg)] fn(string $name) => [new FieldValidationFail($name, $msg == null ? "le champ ne valide pas le pattern $regex" : $msg)]
); );
} }
public static function hex(?string $msg = null): Validator {
return self::regex('/#([0-9a-fA-F])/', $msg == null ? "le champ n'est pas un nombre hexadecimal valide" : $msg);
}
public static function hexColor(?string $msg = null): Validator {
return self::regex('/#([0-9a-fA-F]{6})/', $msg == null ? "le champ n'est pas une couleur valide" : $msg);
}
/** /**
* @return Validator a validator that validates strings that only contains numbers, letters, accents letters, `-` and `_`. * @return Validator a validator that validates strings that only contains numbers, letters, accents letters, `-` and `_`.
*/ */
@ -51,6 +59,13 @@ class Validators {
); );
} }
public static function email(?string $msg = null): Validator {
return new SimpleFunctionValidator(
fn(string $str) => filter_var($str, FILTER_VALIDATE_EMAIL),
fn(string $name) => [new FieldValidationFail($name, $msg == null ? "addresse mail invalide" : $msg)]
);
}
public static function isInteger(): Validator { public static function isInteger(): Validator {
return self::regex("/^[0-9]+$/"); return self::regex("/^[0-9]+$/");

@ -6,6 +6,12 @@ use IQBall\Core\Data\TacticInfo;
use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\ValidationFail;
class TacticValidator { class TacticValidator {
/**
* @param TacticInfo|null $tactic
* @param int $tacticId
* @param int $ownerId
* @return ValidationFail|null
*/
public static function validateAccess(?TacticInfo $tactic, int $tacticId, int $ownerId): ?ValidationFail { public static function validateAccess(?TacticInfo $tactic, int $tacticId, int $ownerId): ?ValidationFail {
if ($tactic == null) { if ($tactic == null) {
return ValidationFail::notFound("La tactique $tacticId n'existe pas"); return ValidationFail::notFound("La tactique $tacticId n'existe pas");
@ -17,5 +23,4 @@ class TacticValidator {
return null; return null;
} }
} }

Loading…
Cancel
Save