From f9eb6fbb6d0a10bce2b8c5b4ea257d3b4ff71475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DAIM?= Date: Wed, 8 Nov 2023 17:30:06 +0100 Subject: [PATCH 01/75] new branch starting the team part in the MVC, plus a little change about the type Color --- Documentation/team.puml | 3 +++ public/index.php | 3 +++ sql/setup-tables.sql | 8 +++++++- src/Controller/FrontController.php | 8 ++++++++ src/Controller/TeamController.php | 33 ++++++++++++++++++++++++++++++ src/Data/Color.php | 21 +++++++++++++++---- src/Gateway/TeamGateway.php | 23 +++++++++++++++++++++ src/Model/TeamModel.php | 24 ++++++++++++++++++++++ 8 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 Documentation/team.puml create mode 100644 src/Controller/FrontController.php create mode 100644 src/Controller/TeamController.php create mode 100644 src/Gateway/TeamGateway.php create mode 100644 src/Model/TeamModel.php diff --git a/Documentation/team.puml b/Documentation/team.puml new file mode 100644 index 0000000..f775665 --- /dev/null +++ b/Documentation/team.puml @@ -0,0 +1,3 @@ +@startuml +/*todo*/ +@enduml \ No newline at end of file diff --git a/public/index.php b/public/index.php index 4c5290b..88d90fa 100644 --- a/public/index.php +++ b/public/index.php @@ -40,6 +40,9 @@ $router->map("POST", "/submit", fn() => $sampleFormController->submitForm($_POST $router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); $router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); +$teamController = new \App\Controller\TeamController(new \App\Model\TeamModel(),$twig); +$router->map("GET","/team/new", fn()=>$teamController->submitTeam($_POST)); + $match = $router->match(); if ($match == null) { diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 0c6fbe7..70723dc 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -1,8 +1,14 @@ - -- drop tables here DROP TABLE IF EXISTS FormEntries; CREATE TABLE FormEntries(name varchar, description varchar); +CREATE TABLE Team( + id numeric PRIMARY KEY AUTOINCREMENT, + name varchar, + picture varchar, + mainColor varchar, + secondColor varchar +); diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php new file mode 100644 index 0000000..c1a39bc --- /dev/null +++ b/src/Controller/FrontController.php @@ -0,0 +1,8 @@ +model = $model; + $this->twig = $twig; + } + + public function submitTeam(array $request){ + if(isset($request['name']) && isset($request["picture"]) && isset($request["mainColor"]) && isset($request["secondColor"])){ + $result= $this->$model->; + } + else{ + http_response_code(400); + echo "Champ(s) manquant(s)"; + } + } + +} + + diff --git a/src/Data/Color.php b/src/Data/Color.php index f841731..bc9043b 100755 --- a/src/Data/Color.php +++ b/src/Data/Color.php @@ -14,10 +14,7 @@ class Color { * @param int $value 6 bytes unsigned int that represents an RGB color * @throws \InvalidArgumentException if the value is negative or greater than 0xFFFFFF */ - public function __constructor(int $value) { - if ($value < 0 || $value > 0xFFFFFF) { - throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF"); - } + private function __constructor(int $value) { $this->value = $value; } @@ -27,4 +24,20 @@ class Color { public function getValue(): int { return $this->value; } + + public static function from(int $value): Color { + $color = self::tryFrom($value); + if ($color == null) { + throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF"); + } + return $color; + } + + public static function tryFrom(int $value): ?Color { + if ($value < 0 || $value > 0xFFFFFF) { + return null; + } + return new Color($value); + } + } \ No newline at end of file diff --git a/src/Gateway/TeamGateway.php b/src/Gateway/TeamGateway.php new file mode 100644 index 0000000..2bd5faa --- /dev/null +++ b/src/Gateway/TeamGateway.php @@ -0,0 +1,23 @@ +con = $con; + } + + function insert(string $name, string $picture, string $mainColor, string $secondColor) { + $this->con->exec( /* todo */ + "INSERT INTO Team VALUES (:name, :picture)", + [ + ":name" => [$username, PDO::PARAM_STR], + "description" => [$description, PDO::PARAM_STR] + ] + ); + } +} \ No newline at end of file diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php new file mode 100644 index 0000000..b1d4e67 --- /dev/null +++ b/src/Model/TeamModel.php @@ -0,0 +1,24 @@ +gateway = $gateway; + } + + + public function createTeam(string $name, Color $mainColor, Color $secondColor ){ + + } +} \ No newline at end of file From c8df55669983ba7ffbc45490bea2c43f0e385f89 Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Fri, 10 Nov 2023 15:23:21 +0100 Subject: [PATCH 02/75] made progression on the create team almost done --- src/Controller/FrontController.php | 8 ------ src/Controller/TeamController.php | 6 +++-- src/Gateway/TeamGateway.php | 20 +++++++++++---- src/Model/TeamModel.php | 39 +++++++++++++++++++++++++++--- src/Model/Validation.php | 10 ++++++++ 5 files changed, 65 insertions(+), 18 deletions(-) delete mode 100644 src/Controller/FrontController.php create mode 100644 src/Model/Validation.php diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php deleted file mode 100644 index c1a39bc..0000000 --- a/src/Controller/FrontController.php +++ /dev/null @@ -1,8 +0,0 @@ -$model->; + $errors = $this->model->createTeam($request['name'],$request['picture'],$request['mainColor'],$request["secondColor"]); + /*todo gestion des erreurs grace au tableau*/ } else{ http_response_code(400); - echo "Champ(s) manquant(s)"; + /*todo rappeler vue avec parametre pour signaler les pb */ } } diff --git a/src/Gateway/TeamGateway.php b/src/Gateway/TeamGateway.php index 2bd5faa..56992d8 100644 --- a/src/Gateway/TeamGateway.php +++ b/src/Gateway/TeamGateway.php @@ -1,7 +1,10 @@ con = $con; } - function insert(string $name, string $picture, string $mainColor, string $secondColor) { - $this->con->exec( /* todo */ - "INSERT INTO Team VALUES (:name, :picture)", + public function insert(string $name, string $picture, Color $mainColor, Color $secondColor) { + $this->con->exec( + "INSERT INTO Team VALUES (:name, :picture, :mainColor, :secondColor)", [ - ":name" => [$username, PDO::PARAM_STR], - "description" => [$description, PDO::PARAM_STR] + ":name" => [$name, PDO::PARAM_STR], + ":picture" => [$picture, PDO::PARAM_STR], + ":mainColor" => [$mainColor, PDO::PARAM_STR], + ":secondColor" => [$secondColor, PDO::PARAM_STR] ] ); } + + public function listByName($name): array { + /*todo*/ + } + } \ No newline at end of file diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php index b1d4e67..3158423 100644 --- a/src/Model/TeamModel.php +++ b/src/Model/TeamModel.php @@ -1,12 +1,22 @@ gateway = $gateway; } + public function createTeam(string $name,string $picture,int $mainColorValue, int $secondColorValue, array $errors) { - public function createTeam(string $name, Color $mainColor, Color $secondColor ){ + $mainColor = Color::tryFrom($mainColorValue); + $secondColor = Color::tryFrom($secondColorValue); + if( $mainColor == null || $secondColor == null ){ + $errors[] = self::ERROR_INVALID_COLOR; + } + + if(Validation::hasHTMLInjection($name)){ + $errors[] = self::ERROR_INVALID_NAME; + } + + if(filter_var($picture,FILTER_VALIDATE_URL)){ + $errors[] = self::ERROR_INVALID_PICTURE; + } + + if(empty($errors)){ + $this->gateway->insert($name,$picture,$mainColor,$secondColor); + } + + } + public function list(string $search,array $errors):array { + if(Validation::hasHTMLInjection($search)){ + $errors = self::ERROR_INVALID_SEARCH; + } } } \ No newline at end of file diff --git a/src/Model/Validation.php b/src/Model/Validation.php new file mode 100644 index 0000000..ac732d0 --- /dev/null +++ b/src/Model/Validation.php @@ -0,0 +1,10 @@ +]"); + } +} \ No newline at end of file From 5737bd6e6b31631984c75211820ba8848e3148ff Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Fri, 10 Nov 2023 15:26:32 +0100 Subject: [PATCH 03/75] Add start of homepage and FrontControler (don't run it) --- Documentation/php.puml | 8 ++++++ public/index.php | 23 ++++++------------ src/Controller/FrontController.php | 39 ++++++++++++++++++++++++++++++ src/Controller/UserController.php | 0 4 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 Documentation/php.puml create mode 100644 src/Controller/FrontController.php create mode 100644 src/Controller/UserController.php diff --git a/Documentation/php.puml b/Documentation/php.puml new file mode 100644 index 0000000..8641900 --- /dev/null +++ b/Documentation/php.puml @@ -0,0 +1,8 @@ +@startuml + +class FrontController { + - router : AltoRouter + +} + +@enduml \ No newline at end of file diff --git a/public/index.php b/public/index.php index 4c5290b..d1fee44 100644 --- a/public/index.php +++ b/public/index.php @@ -34,19 +34,12 @@ $con = new Connexion(get_database()); $router = new AltoRouter(); $router->setBasePath($basePath); -$sampleFormController = new SampleFormController(new FormResultGateway($con), $twig); -$router->map("GET", "/", fn() => $sampleFormController->displayForm()); -$router->map("POST", "/submit", fn() => $sampleFormController->submitForm($_POST)); -$router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); -$router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); - -$match = $router->match(); - -if ($match == null) { - // TODO redirect to a 404 not found page instead (issue #1) - http_response_code(404); - echo "Page non trouvée"; - exit(1); -} +$frontController = new FrontController($router); + +//$sampleFormController = new SampleFormController(new FormResultGateway($con), $twig); + + + + + -call_user_func($match['target']); diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php new file mode 100644 index 0000000..dcee0b1 --- /dev/null +++ b/src/Controller/FrontController.php @@ -0,0 +1,39 @@ +router = $router; + } + + public function main() { + $this->toRoute(); + + } + + private function toRoute(){ + $this->router->map("GET", "/", fn() => $this->userControler->home()); +// $this->router->map("POST", "/submit", fn() => $sampleFormController->submitForm($_POST)); +// $this->router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); +// $this->router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); + + $match = $router->match(); + + // À remplacer par l'appel du contrôler d'erreur + if ($match == null) { + // TODO redirect to a 404 not found page instead (issue #1) + http_response_code(404); + echo "Page non trouvée"; + exit(1); + } + + call_user_func($match['target']); + } + + +} \ No newline at end of file diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php new file mode 100644 index 0000000..e69de29 From d08defaf65a6491f751018b8debea120d0f90df1 Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Sun, 12 Nov 2023 23:50:58 +0100 Subject: [PATCH 04/75] Added a view for the creation of a team, started the list by name action --- src/Controller/TeamController.php | 18 ++++--- src/Gateway/TeamGateway.php | 7 ++- src/Model/TeamModel.php | 13 ++++- src/Views/insertTeam.html.twig | 81 +++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 src/Views/insertTeam.html.twig diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index 930fe21..65e3f25 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -20,16 +20,22 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ public function submitTeam(array $request){ $errors = []; - if(isset($request['name']) && isset($request["picture"]) && isset($request["mainColor"]) && isset($request["secondColor"])){ - $errors = $this->model->createTeam($request['name'],$request['picture'],$request['mainColor'],$request["secondColor"]); - /*todo gestion des erreurs grace au tableau*/ + $this->model->createTeam($request['name'],$request['picture'],$request['mainColor'],$request["secondColor"],$errors); + if(!empty($errors)){ + /*todo appelle vue avec param*/ + } + } + + public function listTeamByName(array $request){ + $errors = []; + $this->model->listByName($request['name'],$errors); + if(!empty($errors)){ + /*todo appelle vue avec param*/ } else{ - http_response_code(400); - /*todo rappeler vue avec parametre pour signaler les pb */ + /*todo appelle bonne vue*/ } } - } diff --git a/src/Gateway/TeamGateway.php b/src/Gateway/TeamGateway.php index 56992d8..17462ea 100644 --- a/src/Gateway/TeamGateway.php +++ b/src/Gateway/TeamGateway.php @@ -27,7 +27,12 @@ class TeamGateway /* retourne exception par rapport à la validité du paramètr } public function listByName($name): array { - /*todo*/ + return $this->con->fetch( + "SELECT name,picture,mainColor,secondColor FROM Team WHERE name LIKE '% :thing %' ", + [ + ":thing" => [$name, PDO::PARAM_STR] + ] + ); } } \ No newline at end of file diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php index 3158423..6815ad6 100644 --- a/src/Model/TeamModel.php +++ b/src/Model/TeamModel.php @@ -16,6 +16,7 @@ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) po public const ERROR_INVALID_NAME = 1; public const ERROR_INVALID_PICTURE = 2; public const ERROR_INVALID_SEARCH = 3; + public const ERROR_NO_DATA_FOUND = 4; private TeamGateway $gateway; @@ -49,9 +50,17 @@ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) po } - public function list(string $search,array $errors):array { - if(Validation::hasHTMLInjection($search)){ + public function listByName(string $name,array $errors):?array { + if(Validation::hasHTMLInjection($name)){ $errors = self::ERROR_INVALID_SEARCH; } + $results = $this->gateway->listByName($name); + if(empty($results)){ + $errors = self::ERROR_NO_DATA_FOUND; + } + if(!empty($errors)){ + return $results; + } + return null; } } \ No newline at end of file diff --git a/src/Views/insertTeam.html.twig b/src/Views/insertTeam.html.twig new file mode 100644 index 0000000..62f6553 --- /dev/null +++ b/src/Views/insertTeam.html.twig @@ -0,0 +1,81 @@ + + + + + Insertion view + + + + + + + +
+

Créer une équipe

+
+
+ + + + + + + + +
+
+ +
+
+
+ + + \ No newline at end of file From 6ff667e3b8bcecb381f8d5b6f430cda65fa14149 Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Mon, 13 Nov 2023 22:12:51 +0100 Subject: [PATCH 05/75] WIP made a diagram class For team within MVC, players aren't incorporated yet --- Documentation/team.puml | 67 ++++++++++++++++++++++++++++++- public/index.php | 2 +- src/Controller/TeamController.php | 2 +- src/Gateway/TeamGateway.php | 2 +- src/Model/TeamModel.php | 2 +- src/Views/insertTeam.html.twig | 3 -- 6 files changed, 70 insertions(+), 8 deletions(-) diff --git a/Documentation/team.puml b/Documentation/team.puml index f775665..b8374cd 100644 --- a/Documentation/team.puml +++ b/Documentation/team.puml @@ -1,3 +1,68 @@ @startuml -/*todo*/ +class Team { + - name: String + - picture: Url + - members: array + + + getName(): String + + getPicture(): Url + + getMainColor(): Color + + getSecondColor(): Color + + listMembers(): array +} + +Team --> "- mainColor" Color +Team --> "- secondaryColor" Color + +class Color { + - value: int + + + getValue(): int +} + +class TeamGateway{ + -- + + __construct(con : Connexion) + + insert(name : string ,picture : string, mainColor : Color, secondColor : Color) + + listByName(name : string): array +} + +TeamGateway *--"- con" Connexion +TeamGateway ..> Color + +class TeamModel{ + + ERROR_INVALID_COLOR : int {readOnly} + + ERROR_INVALID_NAME : int {readOnly} + + ERROR_INVALID_PICTURE : int {readOnly} + + ERROR_INVALID_SEARCH : int {readOnly} + + ERROR_NO_DATA_FOUND : int {readOnly} + --- + + __construct(gateway : TeamGateway) + + createTeam(name : string,picture : string, mainColorValue : int, secondColorValue : int, errors : array) + + listByName(name : string ,errors : array) : ?array +} + +TeamModel *--"- gateway" TeamGateway +TeamModel ..> Team +TeamModel ..> Color + +class TeamController{ + - twig : Environement + -- + + __construct( model : TeamModel, twig : Environement) + + submitTeam(request : array) + + listTeamByName(request : array) +} + +TeamController *--"- model" TeamModel + +class Connexion{ + - pdo : PDO + -- + + __constructor(pdo : PDO) + + exec(query : string, args : array) + + fetch(query string, args array): array + +} + @enduml \ No newline at end of file diff --git a/public/index.php b/public/index.php index 88d90fa..816517d 100644 --- a/public/index.php +++ b/public/index.php @@ -40,7 +40,7 @@ $router->map("POST", "/submit", fn() => $sampleFormController->submitForm($_POST $router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); $router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); -$teamController = new \App\Controller\TeamController(new \App\Model\TeamModel(),$twig); +$teamController = new \App\Controller\TeamController(new \App\Model\TeamModel(new \App\Gateway\TeamGateway($con)),$twig); $router->map("GET","/team/new", fn()=>$teamController->submitTeam($_POST)); $match = $router->match(); diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index 65e3f25..04d88e4 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -28,7 +28,7 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ public function listTeamByName(array $request){ $errors = []; - $this->model->listByName($request['name'],$errors); + $results = $this->model->listByName($request['name'],$errors); if(!empty($errors)){ /*todo appelle vue avec param*/ } diff --git a/src/Gateway/TeamGateway.php b/src/Gateway/TeamGateway.php index 17462ea..f492411 100644 --- a/src/Gateway/TeamGateway.php +++ b/src/Gateway/TeamGateway.php @@ -26,7 +26,7 @@ class TeamGateway /* retourne exception par rapport à la validité du paramètr ); } - public function listByName($name): array { + public function listByName(string $name): array { return $this->con->fetch( "SELECT name,picture,mainColor,secondColor FROM Team WHERE name LIKE '% :thing %' ", [ diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php index 6815ad6..d356672 100644 --- a/src/Model/TeamModel.php +++ b/src/Model/TeamModel.php @@ -10,7 +10,7 @@ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) po { /** - * Attributes used to + * Attributes used to handle errors */ public const ERROR_INVALID_COLOR = 0; public const ERROR_INVALID_NAME = 1; diff --git a/src/Views/insertTeam.html.twig b/src/Views/insertTeam.html.twig index 62f6553..0f68a9f 100644 --- a/src/Views/insertTeam.html.twig +++ b/src/Views/insertTeam.html.twig @@ -55,9 +55,6 @@ - - -

Créer une équipe

From ec474617035ab589fdc5e6ee2fe385c40b58b78f Mon Sep 17 00:00:00 2001 From: d_yanis Date: Tue, 14 Nov 2023 15:57:24 +0100 Subject: [PATCH 06/75] wip FRONT CONTROLLER --- composer.json | 1 + public/index.php | 11 +++-------- src/Controller/FrontController.php | 8 +++++--- src/Controller/UserController.php | 13 +++++++++++++ 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index c78fb15..0f5e1f2 100644 --- a/composer.json +++ b/composer.json @@ -11,4 +11,5 @@ "ext-pdo_sqlite": "*", "twig/twig":"^2.0" } + } \ No newline at end of file diff --git a/public/index.php b/public/index.php index d1fee44..d4e227e 100644 --- a/public/index.php +++ b/public/index.php @@ -3,6 +3,7 @@ require "../vendor/autoload.php"; require "../config.php"; require "../sql/database.php"; +require "../src/Controller/FrontController.php"; use \Twig\Loader\FilesystemLoader; use App\Connexion; @@ -34,12 +35,6 @@ $con = new Connexion(get_database()); $router = new AltoRouter(); $router->setBasePath($basePath); -$frontController = new FrontController($router); - -//$sampleFormController = new SampleFormController(new FormResultGateway($con), $twig); - - - - - +$frontController = new FrontController($router, new UserController()); +//$sampleFormController = new SampleFormController(new FormResultGateway($con), $twig); \ No newline at end of file diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index dcee0b1..7a1179b 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -1,14 +1,16 @@ router = $router; + $this->userControler = $userControler; } public function main() { @@ -22,7 +24,7 @@ class FrontController{ // $this->router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); // $this->router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); - $match = $router->match(); + $match = $this->router->match(); // À remplacer par l'appel du contrôler d'erreur if ($match == null) { diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index e69de29..0934a04 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -0,0 +1,13 @@ +Welcome!"; + } + +} +?> \ No newline at end of file From 02dbf269f966f6a0ded0cd01748342b9bd3e3b8d Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Fri, 10 Nov 2023 15:26:32 +0100 Subject: [PATCH 07/75] Add start of homepage and FrontControler (don't run it) Rebase modification --- Documentation/php.puml | 8 ++++ public/index.php | 51 ++------------------- src/Controller/FrontController.php | 72 ++++++++++++++++++++++++++++++ src/Controller/UserController.php | 0 4 files changed, 84 insertions(+), 47 deletions(-) create mode 100644 Documentation/php.puml create mode 100644 src/Controller/FrontController.php create mode 100644 src/Controller/UserController.php diff --git a/Documentation/php.puml b/Documentation/php.puml new file mode 100644 index 0000000..8641900 --- /dev/null +++ b/Documentation/php.puml @@ -0,0 +1,8 @@ +@startuml + +class FrontController { + - router : AltoRouter + +} + +@enduml \ No newline at end of file diff --git a/public/index.php b/public/index.php index ba9d7c0..9b36d85 100644 --- a/public/index.php +++ b/public/index.php @@ -18,8 +18,7 @@ use App\Validation\ValidationFail; use App\Controller\ErrorController; -$loader = new FilesystemLoader('../src/Views/'); -$twig = new \Twig\Environment($loader); + $basePath = get_public_path(); $con = new Connexion(get_database()); @@ -28,49 +27,7 @@ $con = new Connexion(get_database()); $router = new AltoRouter(); $router->setBasePath($basePath); -$sampleFormController = new SampleFormController(new FormResultGateway($con), $twig); -$editorController = new EditorController(new TacticModel(new TacticInfoGateway($con))); - - -$router->map("GET", "/", fn() => $sampleFormController->displayFormReact()); -$router->map("POST", "/submit", fn() => $sampleFormController->submitFormReact($_POST)); -$router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); -$router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); -$router->map("GET", "/tactic/new", fn() => $editorController->makeNew()); -$router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->openEditorFor($id)); - -$match = $router->match(); - -if ($match == null) { - http_response_code(404); - ErrorController::displayFailures([ValidationFail::notFound("Cette page n'existe pas")], $twig); - return; -} - -$response = call_user_func_array($match['target'], $match['params']); - -http_response_code($response->getCode()); - -if ($response instanceof ViewHttpResponse) { - $file = $response->getFile(); - $args = $response->getArguments(); - - switch ($response->getViewKind()) { - case ViewHttpResponse::REACT_VIEW: - send_react_front($file, $args); - break; - case ViewHttpResponse::TWIG_VIEW: - try { - $twig->display($file, $args); - } catch (\Twig\Error\RuntimeError|\Twig\Error\SyntaxError $e) { - http_response_code(500); - echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); - throw e; - } - break; - } +//$sampleFormController = new SampleFormController(new FormResultGateway($con), $twig); +//$editorController = new EditorController(new TacticModel(new TacticInfoGateway($con))); -} else if ($response instanceof JsonHttpResponse) { - header('Content-type: application/json'); - echo $response->getJson(); -} \ No newline at end of file +$frontController = new FrontController($router); \ No newline at end of file diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php new file mode 100644 index 0000000..5963292 --- /dev/null +++ b/src/Controller/FrontController.php @@ -0,0 +1,72 @@ +router = $router; + } + + public function main() { + $this->toRoute(); + + } + + private function toRoute(){ + $this->router->map("GET", "/", fn() => $this->userControler->home()); +// $router->map("GET", "/", fn() => $sampleFormController->displayFormReact()); +// $router->map("POST", "/submit", fn() => $sampleFormController->submitFormReact($_POST)); +// $router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); +// $router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); +// $router->map("GET", "/tactic/new", fn() => $editorController->makeNew()); +// $router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->openEditorFor($id)); + + + $match = $this->router->match(); + + if ($match == null) { + http_response_code(404); + ErrorController::displayFailures([ValidationFail::notFound("Cette page n'existe pas")], $twig); + return; + } + + $response = call_user_func_array($match['target'], $match['params']); + if ($response instanceof ViewHttpResponse) { + $file = $response->getFile(); + $args = $response->getArguments(); + + switch ($response->getViewKind()) { + case ViewHttpResponse::REACT_VIEW: + send_react_front($file, $args); + break; + case ViewHttpResponse::TWIG_VIEW: + try { + $loader = new FilesystemLoader('../src/Views/'); + $twig = new \Twig\Environment($loader); + $twig->display($file, $args); + } catch (\Twig\Error\RuntimeError|\Twig\Error\SyntaxError $e) { + http_response_code(500); + echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); + throw e; + } + break; + } + + } else if ($response instanceof JsonHttpResponse) { + header('Content-type: application/json'); + echo $response->getJson(); + } + + http_response_code($response->getCode()); + } + + +} \ No newline at end of file diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php new file mode 100644 index 0000000..e69de29 From a283c4b12627feb38ca5cb8c57ea84babed10e43 Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Tue, 14 Nov 2023 17:27:03 +0100 Subject: [PATCH 08/75] WIP almost done, added errors handling and more, got some errors with the gateway, i will fix it tomorrow --- public/index.php | 3 +- sql/setup-tables.sql | 14 ++++++-- src/Controller/TeamController.php | 55 +++++++++++++++++++++++-------- src/Data/Team.php | 2 +- src/Gateway/TeamGateway.php | 16 ++++----- src/Model/TeamModel.php | 33 ++++++------------- src/Model/Validation.php | 10 ------ src/Validation/Validators.php | 19 +++++++++++ src/Views/insertTeam.html.twig | 7 ++++ 9 files changed, 100 insertions(+), 59 deletions(-) delete mode 100644 src/Model/Validation.php diff --git a/public/index.php b/public/index.php index 6bc77c4..ae49e19 100644 --- a/public/index.php +++ b/public/index.php @@ -40,7 +40,8 @@ $router->map("GET", "/tactic/new", fn() => $editorController->makeNew()); $router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->openEditorFor($id)); $teamController = new \App\Controller\TeamController(new \App\Model\TeamModel(new \App\Gateway\TeamGateway($con)),$twig); -$router->map("GET","/team/new", fn()=>$teamController->submitTeam($_POST)); +$router->map("GET","/team/new", fn()=>$teamController->displaySubmitTeam()); +$router->map("POST","/team/new", fn()=>$teamController->SubmitTeam($_POST)); $match = $router->match(); diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index be0301a..989f241 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -8,10 +8,20 @@ CREATE TABLE Team( id numeric PRIMARY KEY AUTOINCREMENT, name varchar, picture varchar, - mainColor varchar, - secondColor varchar + mainColor numeric, + secondColor numeric ); +CREATE TABLE Participate( + idTeam numeric FOREIGN KEY REFERENCES Team(id), + idMember numeric FOREIGN KEY REFERENCES Member(id), + role char CHECK (role IN ('C','P')) +); + +CREATE TABLE Member( + id numeric PRIMARY KEY AUTOINCREMENT, + email varchar, +); CREATE TABLE TacticInfo( id integer PRIMARY KEY AUTOINCREMENT, diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index 04d88e4..de98d09 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -1,7 +1,17 @@ model = $model; $this->twig = $twig; } - public function submitTeam(array $request){ - $errors = []; - $this->model->createTeam($request['name'],$request['picture'],$request['mainColor'],$request["secondColor"],$errors); - if(!empty($errors)){ - /*todo appelle vue avec param*/ + public function displaySubmitTeam() { + try { + $this->twig->display("insertTeam.html.twig", []); + } catch (LoaderError | RuntimeError | SyntaxError $e) { + echo " twig error : $e"; } } - public function listTeamByName(array $request){ + public function submitTeam(array $request): HttpResponse { + $errors = []; - $results = $this->model->listByName($request['name'],$errors); - if(!empty($errors)){ - /*todo appelle vue avec param*/ - } - else{ - /*todo appelle bonne vue*/ + $request = HttpRequest::from($request, $errors, [ + "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], + "mainColor" => [Validators::isInteger(), Validators::isIntInRange(0, 0xffffff)], + "secondColor" => [Validators::isInteger(), Validators::isIntInRange(0, 0xffffff)], + "picture" => [Validators::isURL()] + ]); + if (!empty($errors)) { + $badFields = []; + foreach ($errors as $e) { + if ($e instanceof FieldValidationFail) { + $badFields[] = $e->getFieldName(); + } + } + return ViewHttpResponse::twig('insertTeam.html.twig',['bad_fields'=> $badFields]); } + $this->model->createTeam($request['name'], $request['picture'], intval($request['mainColor']), intval($request['secondColor'])); + return ViewHttpResponse::twig('sample_form.html.twig',[]); + } + + public function listTeamByName(array $request): HttpResponse { + $errors = []; + + $results = $this->model->listByName($request['name'], $errors); + } } diff --git a/src/Data/Team.php b/src/Data/Team.php index 48643d9..bcdabac 100755 --- a/src/Data/Team.php +++ b/src/Data/Team.php @@ -22,7 +22,7 @@ class Team { * @param Color $secondColor * @param array $members */ - public function __construct(string $name, Url $picture, Color $mainColor, Color $secondColor, array $members) { + public function __construct(string $name, Url $picture, Color $mainColor, Color $secondColor, array $members =[]) { $this->name = $name; $this->picture = $picture; $this->mainColor = $mainColor; diff --git a/src/Gateway/TeamGateway.php b/src/Gateway/TeamGateway.php index f492411..7c5883c 100644 --- a/src/Gateway/TeamGateway.php +++ b/src/Gateway/TeamGateway.php @@ -6,7 +6,7 @@ use App\Connexion; use App\Data\Color; use PDO; -class TeamGateway /* retourne exception par rapport à la validité du paramètre par ex. un int qui ne peut pas etre <0 doit etre verif etsoulever une exception */ +class TeamGateway { private Connexion $con; @@ -14,23 +14,23 @@ class TeamGateway /* retourne exception par rapport à la validité du paramètr $this->con = $con; } - public function insert(string $name, string $picture, Color $mainColor, Color $secondColor) { + public function insert(string $name, string $picture, int $mainColor, int $secondColor) { $this->con->exec( - "INSERT INTO Team VALUES (:name, :picture, :mainColor, :secondColor)", + "INSERT INTO Team VALUES (:teamName , :picture, :mainColor, :secondColor)", [ - ":name" => [$name, PDO::PARAM_STR], + ":teamName" => [$name, PDO::PARAM_STR], ":picture" => [$picture, PDO::PARAM_STR], - ":mainColor" => [$mainColor, PDO::PARAM_STR], - ":secondColor" => [$secondColor, PDO::PARAM_STR] + ":mainColor" => [$mainColor, PDO::PARAM_INT], + ":secondColor" => [$secondColor, PDO::PARAM_INT] ] ); } public function listByName(string $name): array { return $this->con->fetch( - "SELECT name,picture,mainColor,secondColor FROM Team WHERE name LIKE '% :thing %' ", + "SELECT name,picture,mainColor,secondColor FROM Team WHERE name LIKE '%:match%' ", [ - ":thing" => [$name, PDO::PARAM_STR] + ":match" => [$name, PDO::PARAM_STR] ] ); } diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php index d356672..4bcd27e 100644 --- a/src/Model/TeamModel.php +++ b/src/Model/TeamModel.php @@ -2,6 +2,7 @@ namespace App\Model; use App\Data\Color; use App\Gateway\TeamGateway; +use App\Data\Team; /** * @@ -28,39 +29,25 @@ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) po $this->gateway = $gateway; } - public function createTeam(string $name,string $picture,int $mainColorValue, int $secondColorValue, array $errors) { - - $mainColor = Color::tryFrom($mainColorValue); - $secondColor = Color::tryFrom($secondColorValue); - if( $mainColor == null || $secondColor == null ){ - $errors[] = self::ERROR_INVALID_COLOR; - } - - if(Validation::hasHTMLInjection($name)){ - $errors[] = self::ERROR_INVALID_NAME; - } - - if(filter_var($picture,FILTER_VALIDATE_URL)){ - $errors[] = self::ERROR_INVALID_PICTURE; - } - - if(empty($errors)){ - $this->gateway->insert($name,$picture,$mainColor,$secondColor); - } + public function createTeam(string $name,string $picture,int $mainColor, int $secondColor) { + $this->gateway->insert($name,$picture,$mainColor,$secondColor); } - public function listByName(string $name,array $errors):?array { + public function listByName(string $name,array &$errors):array { + $teams=[]; if(Validation::hasHTMLInjection($name)){ $errors = self::ERROR_INVALID_SEARCH; } $results = $this->gateway->listByName($name); + if(empty($results)){ $errors = self::ERROR_NO_DATA_FOUND; } - if(!empty($errors)){ - return $results; + + foreach ($results as $row){ + $teams[] = new Team($row['name'],$row['picture'],$row['mainColor'],$row['secondColor']); } - return null; + return $results; } } \ No newline at end of file diff --git a/src/Model/Validation.php b/src/Model/Validation.php deleted file mode 100644 index ac732d0..0000000 --- a/src/Model/Validation.php +++ /dev/null @@ -1,10 +0,0 @@ -]"); - } -} \ No newline at end of file diff --git a/src/Validation/Validators.php b/src/Validation/Validators.php index ea9da46..2f69ca4 100644 --- a/src/Validation/Validators.php +++ b/src/Validation/Validators.php @@ -51,4 +51,23 @@ class Validators { } ); } + + public static function isInteger(): Validator { + return self::regex("/^[0-9]+$/"); + } + + public static function isIntInRange(int $min,int $max): Validator { + return new SimpleFunctionValidator( + fn(string $val) => intval($val) >= $min && intval($val) <= $max, + fn(string $name) => [new FieldValidationFail($name, "The value is not in the range $min to $max ")] + ); + } + + public static function isURL(): Validator { + return new SimpleFunctionValidator( + fn($val) => filter_var($val, FILTER_VALIDATE_URL) , + fn(string $name) => [new FieldValidationFail($name, "The value is not an URL")] + ); + } + } \ No newline at end of file diff --git a/src/Views/insertTeam.html.twig b/src/Views/insertTeam.html.twig index 0f68a9f..ba6ffac 100644 --- a/src/Views/insertTeam.html.twig +++ b/src/Views/insertTeam.html.twig @@ -31,6 +31,12 @@ margin-bottom: 5px; } + {% for item in bad_fields %} + #{{ item }}{ + border-color: red; + } + {% endfor %} + input[type="text"], input[type="password"] { width: 100%; padding: 10px; @@ -51,6 +57,7 @@ background-color: #0056b3; } + From 26a27c291d4d643f94f79b6fd22b5b48784b3b73 Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Tue, 14 Nov 2023 17:35:01 +0100 Subject: [PATCH 09/75] Add FrontController and UserController --- public/index.php | 7 +-- src/Controller/FrontController.php | 72 +++++++++++++++++++----------- src/Controller/UserController.php | 17 +++++++ src/Views/home.twig | 13 ++++++ 4 files changed, 80 insertions(+), 29 deletions(-) create mode 100644 src/Views/home.twig diff --git a/public/index.php b/public/index.php index 9b36d85..4ccdf44 100644 --- a/public/index.php +++ b/public/index.php @@ -6,6 +6,7 @@ require "../sql/database.php"; require "utils.php"; use App\Connexion; +use App\Controller\FrontController; use App\Controller\EditorController; use App\Controller\SampleFormController; use App\Gateway\FormResultGateway; @@ -18,8 +19,6 @@ use App\Validation\ValidationFail; use App\Controller\ErrorController; - - $basePath = get_public_path(); $con = new Connexion(get_database()); @@ -27,7 +26,9 @@ $con = new Connexion(get_database()); $router = new AltoRouter(); $router->setBasePath($basePath); +$frontController = new FrontController($router); +$frontController->route(); + //$sampleFormController = new SampleFormController(new FormResultGateway($con), $twig); //$editorController = new EditorController(new TacticModel(new TacticInfoGateway($con))); -$frontController = new FrontController($router); \ No newline at end of file diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index 5963292..b43023e 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -1,27 +1,37 @@ router = $router; + $this->userController = new UserController(); } - public function main() { - $this->toRoute(); + public function run() { + $this->route(); } - private function toRoute(){ - $this->router->map("GET", "/", fn() => $this->userControler->home()); + public function route() + { + $this->router->map("GET", "/", fn() => $this->userController->home()); // $router->map("GET", "/", fn() => $sampleFormController->displayFormReact()); // $router->map("POST", "/submit", fn() => $sampleFormController->submitFormReact($_POST)); // $router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); @@ -33,39 +43,49 @@ class FrontController{ $match = $this->router->match(); if ($match == null) { - http_response_code(404); + $loader = new FilesystemLoader('../src/Views/'); + $twig = new \Twig\Environment($loader); + http_response_code(HttpCodes::NOT_FOUND); ErrorController::displayFailures([ValidationFail::notFound("Cette page n'existe pas")], $twig); return; } + $this->routeByResponseType($match); + } + private function routeByResponseType(array $match) + { $response = call_user_func_array($match['target'], $match['params']); + http_response_code($response->getCode()); if ($response instanceof ViewHttpResponse) { - $file = $response->getFile(); - $args = $response->getArguments(); - - switch ($response->getViewKind()) { - case ViewHttpResponse::REACT_VIEW: - send_react_front($file, $args); - break; - case ViewHttpResponse::TWIG_VIEW: - try { - $loader = new FilesystemLoader('../src/Views/'); - $twig = new \Twig\Environment($loader); - $twig->display($file, $args); - } catch (\Twig\Error\RuntimeError|\Twig\Error\SyntaxError $e) { - http_response_code(500); - echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); - throw e; - } - break; - } + + $this->displayByViewKind($response); } else if ($response instanceof JsonHttpResponse) { header('Content-type: application/json'); echo $response->getJson(); } + } - http_response_code($response->getCode()); + private function displayByViewKind(ViewHttpResponse $response){ + $file = $response->getFile(); + $args = $response->getArguments(); + + switch ($response->getViewKind()) { + case ViewHttpResponse::REACT_VIEW: + send_react_front($file, $args); + break; + case ViewHttpResponse::TWIG_VIEW: + try { + $loader = new FilesystemLoader('../src/Views/'); + $twig = new \Twig\Environment($loader); + $twig->display($file, $args); + } catch (\Twig\Error\RuntimeError | \Twig\Error\SyntaxError $e) { + http_response_code(500); + echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); + throw e; + } + break; + } } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index e69de29..5a7d85a 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/src/Views/home.twig b/src/Views/home.twig new file mode 100644 index 0000000..8b5ce94 --- /dev/null +++ b/src/Views/home.twig @@ -0,0 +1,13 @@ + + + + + + + Document + + +

Test

+ + \ No newline at end of file From 051dbef9ed88597ea0baa94c2f10dae9c96beab2 Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Tue, 14 Nov 2023 17:37:23 +0100 Subject: [PATCH 10/75] Add FrontController and UserController --- src/Controller/FrontController.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index b43023e..c8b260d 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -26,7 +26,6 @@ class FrontController{ public function run() { $this->route(); - } public function route() @@ -49,10 +48,10 @@ class FrontController{ ErrorController::displayFailures([ValidationFail::notFound("Cette page n'existe pas")], $twig); return; } - $this->routeByResponseType($match); + $this->handleByResponseType($match); } - private function routeByResponseType(array $match) + private function handleByResponseType(array $match) { $response = call_user_func_array($match['target'], $match['params']); http_response_code($response->getCode()); From 0859467e3fdda12a5cef0e62aba7e1a089b04277 Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Tue, 14 Nov 2023 22:58:21 +0100 Subject: [PATCH 11/75] WIP just a few little adjusments and progress --- public/index.php | 3 + sql/setup-tables.sql | 4 +- src/Controller/TeamController.php | 28 +++++-- src/Model/TeamModel.php | 11 +-- ...rtTeam.html.twig => insert_team.html.twig} | 0 src/Views/list_team_by_name.html.twig | 77 +++++++++++++++++++ 6 files changed, 106 insertions(+), 17 deletions(-) rename src/Views/{insertTeam.html.twig => insert_team.html.twig} (100%) create mode 100644 src/Views/list_team_by_name.html.twig diff --git a/public/index.php b/public/index.php index ae49e19..bdb9000 100644 --- a/public/index.php +++ b/public/index.php @@ -42,6 +42,9 @@ $router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->ope $teamController = new \App\Controller\TeamController(new \App\Model\TeamModel(new \App\Gateway\TeamGateway($con)),$twig); $router->map("GET","/team/new", fn()=>$teamController->displaySubmitTeam()); $router->map("POST","/team/new", fn()=>$teamController->SubmitTeam($_POST)); +$router->map("GET","/team/list", fn()=>$teamController->displayListTeamByName()); +$router->map("POST","/team/list", fn()=>$teamController->ListTeamByName($_POST)); +/*todo $router->map("GET","/team/[i:id]", fn()=>$teamController->displayTeam);*/ $match = $router->match(); diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 989f241..437649c 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -5,7 +5,7 @@ DROP TABLE IF EXISTS TacticInfo; CREATE TABLE FormEntries(name varchar, description varchar); CREATE TABLE Team( - id numeric PRIMARY KEY AUTOINCREMENT, + id integer PRIMARY KEY AUTOINCREMENT, name varchar, picture varchar, mainColor numeric, @@ -19,7 +19,7 @@ CREATE TABLE Participate( ); CREATE TABLE Member( - id numeric PRIMARY KEY AUTOINCREMENT, + id integer PRIMARY KEY AUTOINCREMENT, email varchar, ); diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index de98d09..8702b3e 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -29,7 +29,7 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ public function displaySubmitTeam() { try { - $this->twig->display("insertTeam.html.twig", []); + $this->twig->display("insert_team.html.twig", []); } catch (LoaderError | RuntimeError | SyntaxError $e) { echo " twig error : $e"; } @@ -51,17 +51,33 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ $badFields[] = $e->getFieldName(); } } - return ViewHttpResponse::twig('insertTeam.html.twig',['bad_fields'=> $badFields]); + return ViewHttpResponse::twig('insert_team.html.twig',['bad_fields'=> $badFields]); } $this->model->createTeam($request['name'], $request['picture'], intval($request['mainColor']), intval($request['secondColor'])); - return ViewHttpResponse::twig('sample_form.html.twig',[]); + return ViewHttpResponse::twig('sample_form.html.twig',[]); /*todo appeler une vue qui display la team au lieu de ça*/ } + public function displayListTeamByName(){ + try { + $this->twig->display("list_team_by_name.html.twig", []); + } catch (LoaderError | RuntimeError | SyntaxError $e) { + echo " twig error : $e"; + } + } public function listTeamByName(array $request): HttpResponse { $errors = []; - - $results = $this->model->listByName($request['name'], $errors); - + $request = HttpRequest::from($request, $errors, [ + "name" => [Validators::lenBetween(1,32),Validators::nameWithSpaces()] + ]); + if(!empty($errors) && $errors[0] instanceof FieldValidationFail){ + $badField = $errors[0]->getFieldName(); + return ViewHttpResponse::twig('list_team_by_name.html.twig',['bad_field' => $badField]) ; + } + $results = $this->model->listByName($request['name']); + if (empty($results)){ + /*todo appelle de la bonne vue qui va afficher un message qui dit que bah ca retourne rien, proposer de refaire une recherche*/ + } + return ViewHttpResponse::twig('display_teams.html.twig',['teams' => $results]); } } diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php index 4bcd27e..02eef70 100644 --- a/src/Model/TeamModel.php +++ b/src/Model/TeamModel.php @@ -34,20 +34,13 @@ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) po } - public function listByName(string $name,array &$errors):array { + public function listByName(string $name):array { $teams=[]; - if(Validation::hasHTMLInjection($name)){ - $errors = self::ERROR_INVALID_SEARCH; - } $results = $this->gateway->listByName($name); - if(empty($results)){ - $errors = self::ERROR_NO_DATA_FOUND; - } - foreach ($results as $row){ $teams[] = new Team($row['name'],$row['picture'],$row['mainColor'],$row['secondColor']); } - return $results; + return $teams; } } \ No newline at end of file diff --git a/src/Views/insertTeam.html.twig b/src/Views/insert_team.html.twig similarity index 100% rename from src/Views/insertTeam.html.twig rename to src/Views/insert_team.html.twig diff --git a/src/Views/list_team_by_name.html.twig b/src/Views/list_team_by_name.html.twig new file mode 100644 index 0000000..7e0cbb3 --- /dev/null +++ b/src/Views/list_team_by_name.html.twig @@ -0,0 +1,77 @@ + + + + + Insertion view + + + + +
+

Créer une équipe

+ +
+ + +
+
+ +
+ +
+ + + \ No newline at end of file From 30611b9b21b0b9dad948fdfb5f33dabb12fdadfc Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Wed, 15 Nov 2023 15:21:17 +0100 Subject: [PATCH 12/75] WIP done with insertion, got a problem on list team and starting displayTeam --- public/index.php | 4 ++- src/Controller/TeamController.php | 35 ++++++++++++--------------- src/Data/Team.php | 11 ++++++++- src/Gateway/TeamGateway.php | 28 ++++++++++++++++----- src/Model/TeamModel.php | 20 ++++++--------- src/Views/display_team.html.twig | 35 +++++++++++++++++++++++++++ src/Views/display_teams.html.twig | 35 +++++++++++++++++++++++++++ src/Views/list_team_by_name.html.twig | 2 +- 8 files changed, 129 insertions(+), 41 deletions(-) create mode 100644 src/Views/display_team.html.twig create mode 100644 src/Views/display_teams.html.twig diff --git a/public/index.php b/public/index.php index bdb9000..cc63837 100644 --- a/public/index.php +++ b/public/index.php @@ -42,9 +42,11 @@ $router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->ope $teamController = new \App\Controller\TeamController(new \App\Model\TeamModel(new \App\Gateway\TeamGateway($con)),$twig); $router->map("GET","/team/new", fn()=>$teamController->displaySubmitTeam()); $router->map("POST","/team/new", fn()=>$teamController->SubmitTeam($_POST)); + $router->map("GET","/team/list", fn()=>$teamController->displayListTeamByName()); $router->map("POST","/team/list", fn()=>$teamController->ListTeamByName($_POST)); -/*todo $router->map("GET","/team/[i:id]", fn()=>$teamController->displayTeam);*/ + +$router->map("GET","/team/[i:id]", fn(int $id)=>$teamController->displayTeam($id)); $match = $router->match(); diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index 8702b3e..8228d6a 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -28,11 +28,7 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ } public function displaySubmitTeam() { - try { - $this->twig->display("insert_team.html.twig", []); - } catch (LoaderError | RuntimeError | SyntaxError $e) { - echo " twig error : $e"; - } + return ViewHttpResponse::twig("insert_team.html.twig", []); } public function submitTeam(array $request): HttpResponse { @@ -51,33 +47,34 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ $badFields[] = $e->getFieldName(); } } - return ViewHttpResponse::twig('insert_team.html.twig',['bad_fields'=> $badFields]); + return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]); } $this->model->createTeam($request['name'], $request['picture'], intval($request['mainColor']), intval($request['secondColor'])); - return ViewHttpResponse::twig('sample_form.html.twig',[]); /*todo appeler une vue qui display la team au lieu de ça*/ + return ViewHttpResponse::twig('sample_form.html.twig', []); /*todo appeler une vue qui display la team au lieu de ça*/ } - public function displayListTeamByName(){ - try { - $this->twig->display("list_team_by_name.html.twig", []); - } catch (LoaderError | RuntimeError | SyntaxError $e) { - echo " twig error : $e"; - } + public function displayListTeamByName(): HttpResponse { + return ViewHttpResponse::twig("list_team_by_name.html.twig", []); } + public function listTeamByName(array $request): HttpResponse { $errors = []; $request = HttpRequest::from($request, $errors, [ - "name" => [Validators::lenBetween(1,32),Validators::nameWithSpaces()] + "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()] ]); - if(!empty($errors) && $errors[0] instanceof FieldValidationFail){ + if (!empty($errors) && $errors[0] instanceof FieldValidationFail) { $badField = $errors[0]->getFieldName(); - 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']); - if (empty($results)){ - /*todo appelle de la bonne vue qui va afficher un message qui dit que bah ca retourne rien, proposer de refaire une recherche*/ + if (empty($results)) { + return ViewHttpResponse::twig('display_teams.html.twig', []); } - return ViewHttpResponse::twig('display_teams.html.twig',['teams' => $results]); + return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $results]); + } + + public function displayTeam(int $id): HttpResponse{ + $results = $this->model->displayTeam($id); } } diff --git a/src/Data/Team.php b/src/Data/Team.php index bcdabac..321a5d9 100755 --- a/src/Data/Team.php +++ b/src/Data/Team.php @@ -5,6 +5,7 @@ namespace App\Data; use http\Url; class Team { + private int $id; private string $name; private Url $picture; private Color $mainColor; @@ -22,7 +23,8 @@ class Team { * @param Color $secondColor * @param array $members */ - public function __construct(string $name, Url $picture, Color $mainColor, Color $secondColor, array $members =[]) { + public function __construct(int $id,string $name, Url $picture, Color $mainColor, Color $secondColor, array $members =[]) { + $this->id = $id; $this->name = $name; $this->picture = $picture; $this->mainColor = $mainColor; @@ -30,6 +32,13 @@ class Team { $this->members = $members; } + /** + * @return int + */ + public function getId(): int { + return $this->id; + } + /** * @return string */ diff --git a/src/Gateway/TeamGateway.php b/src/Gateway/TeamGateway.php index 7c5883c..7d9c014 100644 --- a/src/Gateway/TeamGateway.php +++ b/src/Gateway/TeamGateway.php @@ -3,11 +3,9 @@ namespace App\Gateway; use App\Connexion; -use App\Data\Color; use PDO; -class TeamGateway -{ +class TeamGateway { private Connexion $con; public function __construct(Connexion $con) { @@ -16,7 +14,7 @@ class TeamGateway public function insert(string $name, string $picture, int $mainColor, int $secondColor) { $this->con->exec( - "INSERT INTO Team VALUES (:teamName , :picture, :mainColor, :secondColor)", + "INSERT INTO Team(name, picture, mainColor, secondColor) VALUES (:teamName , :picture, :mainColor, :secondColor)", [ ":teamName" => [$name, PDO::PARAM_STR], ":picture" => [$picture, PDO::PARAM_STR], @@ -28,11 +26,29 @@ class TeamGateway public function listByName(string $name): array { return $this->con->fetch( - "SELECT name,picture,mainColor,secondColor FROM Team WHERE name LIKE '%:match%' ", + "SELECT id,name,picture,mainColor,secondColor FROM Team WHERE name LIKE '%' || :name || '%'", [ - ":match" => [$name, PDO::PARAM_STR] + ":name" => [$name, PDO::PARAM_STR] ] ); } + public function getTeamById(int $id): array{ + return $this->con->fetch( + "SELECT name,picture,mainColor,secondColor FROM Team WHERE id = :id", + [ + ":id" => [$id, PDO::PARAM_INT] + ] + ); + } + + public function getMembersById($id):array{ + return $this->con->fetch( + "SELECT p.role,m.email FROM Member m,Team t,Participate p WHERE t.id = :id AND p.idTeam = t.id AND p.idMember = m.id", + [ + ":id" => [$id, PDO::PARAM_INT] + ] + ); + } + } \ No newline at end of file diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php index 02eef70..30c9fde 100644 --- a/src/Model/TeamModel.php +++ b/src/Model/TeamModel.php @@ -9,16 +9,6 @@ use App\Data\Team; */ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) pour le controller qui l'utilise, catch celle de la gw, */ { - - /** - * Attributes used to handle errors - */ - public const ERROR_INVALID_COLOR = 0; - public const ERROR_INVALID_NAME = 1; - public const ERROR_INVALID_PICTURE = 2; - public const ERROR_INVALID_SEARCH = 3; - public const ERROR_NO_DATA_FOUND = 4; - private TeamGateway $gateway; /** @@ -31,16 +21,20 @@ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) po public function createTeam(string $name,string $picture,int $mainColor, int $secondColor) { $this->gateway->insert($name,$picture,$mainColor,$secondColor); - } public function listByName(string $name):array { $teams=[]; $results = $this->gateway->listByName($name); - foreach ($results as $row){ - $teams[] = new Team($row['name'],$row['picture'],$row['mainColor'],$row['secondColor']); + $teams[] = new Team($row['id'],$row['name'],$row['picture'],$row['mainColor'],$row['secondColor']); } return $teams; } + + public function displayTeam(int $id): array{ + $resultTeam = $this->gateway->getTeamById($id); + $resultMembers = $this->gateway->getMembersById($id); + + } } \ No newline at end of file diff --git a/src/Views/display_team.html.twig b/src/Views/display_team.html.twig new file mode 100644 index 0000000..a1f88fc --- /dev/null +++ b/src/Views/display_team.html.twig @@ -0,0 +1,35 @@ + + + + + Twig view + + + +

Hello world

+ +{% if teams is empty %} +

Aucune équipe n'a été trouvée

+
+

Chercher une équipe

+
+
+ + +
+
+ +
+
+
+{% else %} + {% for t in teams %} +
+

Nom de l'équipe: {{ t.name }}

+

picture : {{ t.picture }}

+
+ {% endfor %} +{% endif %} + + + \ No newline at end of file diff --git a/src/Views/display_teams.html.twig b/src/Views/display_teams.html.twig new file mode 100644 index 0000000..a1f88fc --- /dev/null +++ b/src/Views/display_teams.html.twig @@ -0,0 +1,35 @@ + + + + + Twig view + + + +

Hello world

+ +{% if teams is empty %} +

Aucune équipe n'a été trouvée

+
+

Chercher une équipe

+
+
+ + +
+
+ +
+
+
+{% else %} + {% for t in teams %} +
+

Nom de l'équipe: {{ t.name }}

+

picture : {{ t.picture }}

+
+ {% endfor %} +{% endif %} + + + \ No newline at end of file diff --git a/src/Views/list_team_by_name.html.twig b/src/Views/list_team_by_name.html.twig index 7e0cbb3..1d9ddf3 100644 --- a/src/Views/list_team_by_name.html.twig +++ b/src/Views/list_team_by_name.html.twig @@ -61,7 +61,7 @@
-

Créer une équipe

+

Chercher une équipe

From 1acede41693ad7769df3247d7ba85ce00dfc38b9 Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Thu, 16 Nov 2023 13:17:46 +0100 Subject: [PATCH 13/75] WIP did a few little changes --- src/Controller/TeamController.php | 3 ++- src/Data/Member.php | 13 ++++++++++- src/Gateway/TeamGateway.php | 2 +- src/Model/TeamModel.php | 37 +++++++++++++++++++++---------- src/Views/display_team.html.twig | 28 +++++------------------ src/Views/display_teams.html.twig | 2 -- 6 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index 8228d6a..ffbcb8e 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -74,7 +74,8 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ } public function displayTeam(int $id): HttpResponse{ - $results = $this->model->displayTeam($id); + $result = $this->model->displayTeam($id); + return ViewHttpResponse::twig('display_team.html.twig',['team' => $result]); } } diff --git a/src/Data/Member.php b/src/Data/Member.php index 91b09c4..9bf0b52 100755 --- a/src/Data/Member.php +++ b/src/Data/Member.php @@ -11,6 +11,7 @@ class Member { */ private int $userId; + private string $email; /** * @var MemberRole the member's role */ @@ -18,13 +19,16 @@ class Member { /** * @param int $userId + * @param string $email * @param MemberRole $role */ - public function __construct(int $userId, MemberRole $role) { + public function __construct(int $userId, string $email, MemberRole $role) { $this->userId = $userId; + $this->email = $email; $this->role = $role; } + /** * @return int */ @@ -39,4 +43,11 @@ class Member { return $this->role; } + /** + * @return string + */ + public function getEmail(): string { + return $this->email; + } + } \ No newline at end of file diff --git a/src/Gateway/TeamGateway.php b/src/Gateway/TeamGateway.php index 7d9c014..1540c73 100644 --- a/src/Gateway/TeamGateway.php +++ b/src/Gateway/TeamGateway.php @@ -44,7 +44,7 @@ class TeamGateway { public function getMembersById($id):array{ return $this->con->fetch( - "SELECT p.role,m.email FROM Member m,Team t,Participate p WHERE t.id = :id AND p.idTeam = t.id AND p.idMember = m.id", + "SELECT p.role,m.email,m.id FROM Member m,Team t,Participate p WHERE t.id = :id AND p.idTeam = t.id AND p.idMember = m.id", [ ":id" => [$id, PDO::PARAM_INT] ] diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php index 30c9fde..8c5c8f5 100644 --- a/src/Model/TeamModel.php +++ b/src/Model/TeamModel.php @@ -1,8 +1,11 @@ gateway = $gateway; } - public function createTeam(string $name,string $picture,int $mainColor, int $secondColor) { - $this->gateway->insert($name,$picture,$mainColor,$secondColor); + public function createTeam(string $name, string $picture, int $mainColor, int $secondColor) { + $this->gateway->insert($name, $picture, $mainColor, $secondColor); } - public function listByName(string $name):array { - $teams=[]; + public function listByName(string $name): array { + $teams = []; $results = $this->gateway->listByName($name); - foreach ($results as $row){ - $teams[] = new Team($row['id'],$row['name'],$row['picture'],$row['mainColor'],$row['secondColor']); + foreach ($results as $row) { + $teams[] = new Team($row['id'], $row['name'], $row['picture'], $row['mainColor'], $row['secondColor']); } return $teams; } - public function displayTeam(int $id): array{ - $resultTeam = $this->gateway->getTeamById($id); + public function displayTeam(int $id): Team { + $members = []; + $result = $this->gateway->getTeamById($id); $resultMembers = $this->gateway->getMembersById($id); - + foreach ($resultMembers as $row) { + if ($row['role'] == 'C') { + $role = 1; + } else { + $role = 2; + } + $members[] = new Member($row['id'], $row['email'], $role); + } + $team = new Team($result['id'], $result['name'], $result['picture'], $result['mainColor'], $result['secondColor'], $members); + return $team; } + } \ No newline at end of file diff --git a/src/Views/display_team.html.twig b/src/Views/display_team.html.twig index a1f88fc..d06322f 100644 --- a/src/Views/display_team.html.twig +++ b/src/Views/display_team.html.twig @@ -6,30 +6,12 @@ -

Hello world

+

{{ team.name }}

-{% if teams is empty %} -

Aucune équipe n'a été trouvée

-
-

Chercher une équipe

- -
- - -
-
- -
- -
-{% else %} - {% for t in teams %} -
-

Nom de l'équipe: {{ t.name }}

-

picture : {{ t.picture }}

-
- {% endfor %} -{% endif %} + +{% for m in team.members %} +

m.email

+{% endfor %} \ No newline at end of file diff --git a/src/Views/display_teams.html.twig b/src/Views/display_teams.html.twig index a1f88fc..d0c7c22 100644 --- a/src/Views/display_teams.html.twig +++ b/src/Views/display_teams.html.twig @@ -6,8 +6,6 @@ -

Hello world

- {% if teams is empty %}

Aucune équipe n'a été trouvée

From 61a6b5afd5c3e72706981171a0bd0b9be8d7e1b2 Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Fri, 17 Nov 2023 12:06:03 +0100 Subject: [PATCH 14/75] wip FrontController --- Documentation/http.puml | 2 +- public/index.php | 20 ++-- src/Controller/FrontController.php | 135 ++++++++++++++++++------ src/Controller/SampleFormController.php | 52 --------- src/Gateway/FormResultGateway.php | 33 ------ src/Views/home.twig | 2 +- src/Views/sample_form.html.twig | 20 ---- 7 files changed, 117 insertions(+), 147 deletions(-) delete mode 100644 src/Controller/SampleFormController.php delete mode 100644 src/Gateway/FormResultGateway.php delete mode 100644 src/Views/sample_form.html.twig diff --git a/Documentation/http.puml b/Documentation/http.puml index b41135d..9fbe606 100644 --- a/Documentation/http.puml +++ b/Documentation/http.puml @@ -35,7 +35,7 @@ class ViewHttpResponse extends HttpResponse { - arguments: array - kind: int - + __construct(kind: int, file: string, arguments: array, code: int = HttpCodes::OK) + - __construct(kind: int, file: string, arguments: array, code: int = HttpCodes::OK) + getViewKind(): int + getFile(): string + getArguments(): array diff --git a/public/index.php b/public/index.php index 77273f5..f95879f 100644 --- a/public/index.php +++ b/public/index.php @@ -4,6 +4,7 @@ require "../vendor/autoload.php"; require "../config.php"; require "../sql/database.php"; require "utils.php"; +require "../src/react-display.php"; use App\Connexion; @@ -19,18 +20,17 @@ use Twig\Loader\FilesystemLoader; use App\Validation\ValidationFail; use App\Controller\ErrorController; - +session_start(); $basePath = get_public_path(); +var_dump($basePath); $con = new Connexion(get_database()); -// routes initialization -$router = new AltoRouter(); -$router->setBasePath($basePath); - -$frontController = new FrontController($router); -$frontController->route(); - -//$sampleFormController = new SampleFormController(new FormResultGateway($con), $twig); -//$editorController = new EditorController(new TacticModel(new TacticInfoGateway($con))); +global $dictActionRole; +$dictActionRole = [ + "UserController" => "public", + "EditionUserController" => "editor" +]; +$frontController = new FrontController($con, $basePath, $dictActionRole); +$frontController->run(); diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index 9a97466..337b3ca 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -3,13 +3,18 @@ namespace App\Controller; +use App\Connexion; use App\Controller\UserController; use AltoRouter; use App\Controller\ErrorController; +use App\Gateway\FormResultGateway; +use App\Gateway\TacticInfoGateway; use App\Http\HttpCodes; +use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; +use App\Model\TacticModel; use App\Validation\ValidationFail; use Twig\Loader\FilesystemLoader; @@ -17,44 +22,110 @@ use Twig\Loader\FilesystemLoader; class FrontController{ private AltoRouter $router; - private ?UserController $userController; + private Connexion $con; + private array $dictControllerRole; + public function __construct(Connexion $con, string $basePath, array $dictControllerRole) { + $this->con = $con; + $this->router = $this->createRouter($basePath); + $this->dictControllerRole = $dictControllerRole; + } + + /** + * Main behavior of the FrontController + * + * @return void + */ + public function run() : void { + $this->initializeRouterMap(); - public function __construct(AltoRouter $router){ - $this->router = $router; - $this->userController = new UserController(); + $match = $this->router->match(); + if ($this->validMatch($match)){ + $this->roleControl($match['target']); + } else { + $this->displayByViewKind(ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND)); + } + // $this->roleControl($match["target"]); + // $this->handleByResponseType($this->matchRoute()); } - public function run() { - $this->route(); + /** + * Create a new instance of an AltoRouter + * + * @param string $basePath + * @return AltoRouter + */ + public function createRouter(string $basePath) : AltoRouter { + $router = new AltoRouter(); + $router->setBasePath($basePath); + return $router; } - public function route() - { - $this->router->map("GET", "/", fn() => $this->userController->home()); -// $router->map("GET", "/", fn() => $sampleFormController->displayFormReact()); -// $router->map("POST", "/submit", fn() => $sampleFormController->submitFormReact($_POST)); -// $router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); -// $router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); -// $router->map("GET", "/tactic/new", fn() => $editorController->makeNew()); -// $router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->openEditorFor($id)); + /** + * Initialize project's routes + * + * @return void + */ + private function initializeRouterMap() : void { + $this->router->map("GET", "/", "UserController"); + $this->router->map("GET", "/[a:action]?", "UserController"); + $this->router->map("GET", "/tactic/[a:action]/[i:idTactic]?", "EditorController"); + // $this->router->map("GET", "/tactic/[i:id]/edit", "EditorController"); + // $this->router->map("GET", "/", fn() => (new UserController())->home()); + // $this->router->map("GET", "/tactic/new", fn() => (new EditorController(new TacticModel(new TacticInfoGateway($this->con))))->makeNew()); + // $this->router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => (new EditorController(new TacticModel(new TacticInfoGateway($this->con))))->openEditorFor($id)); + } - $match = $this->router->match(); + private function validMatch($match){ + return $match != null; + } - if ($match == null) { - $loader = new FilesystemLoader('../src/Views/'); - $twig = new \Twig\Environment($loader); - http_response_code(HttpCodes::NOT_FOUND); - ErrorController::displayFailures([ValidationFail::notFound("Cette page n'existe pas")], $twig); - return; + /** + * Initialize router's settings + * + * @return ViewHttpResponse + */ + private function roleControl(string $actionController){ + + if (isset($_SESSION['role'])){ + if($_SESSION['role'] = $this->dictControllerRole[$actionController]){ + echo "Là on redirige vers le bon controller"; + } + } + else { + $_SESSION['role'] + echo "session initialisé"; } - $this->handleByResponseType($match); + + + + + + + + // // Page not found + // if ($match == null) { + // return new ViewHttpResponse(ViewHttpResponse::TWIG_VIEW, "Views/error.html.twig", [ValidationFail::notFound("Cette page n'existe pas")], HttpCodes::NOT_FOUND); + // } + // // $loader = new FilesystemLoader('../src/Views/'); + // // $twig = new \Twig\Environment($loader); + // // http_response_code(HttpCodes::NOT_FOUND); + // // ErrorController::displayFailures([ValidationFail::notFound("Cette page n'existe pas")], $twig); + // // return; + // // } + } - private function handleByResponseType(array $match) - { - $response = call_user_func_array($match['target'], $match['params']); + + /** + * Redirect the return of the response by the response's type + * + * @param array $match + * @return void + */ + private function handleByResponseType(HttpResponse $response) : void { + // $response = call_user_func_array($match['target'], $match['params']); http_response_code($response->getCode()); if ($response instanceof ViewHttpResponse) { @@ -66,7 +137,13 @@ class FrontController{ } } - private function displayByViewKind(ViewHttpResponse $response){ + /** + * Use the right method to display the response + * + * @param ViewHttpResponse $response + * @return void + */ + private function displayByViewKind(ViewHttpResponse $response) : void { $file = $response->getFile(); $args = $response->getArguments(); @@ -82,11 +159,9 @@ class FrontController{ } catch (\Twig\Error\RuntimeError | \Twig\Error\SyntaxError $e) { http_response_code(500); echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); - throw e; + throw $e; } break; } } - - } \ No newline at end of file diff --git a/src/Controller/SampleFormController.php b/src/Controller/SampleFormController.php deleted file mode 100644 index 4241ad4..0000000 --- a/src/Controller/SampleFormController.php +++ /dev/null @@ -1,52 +0,0 @@ -gateway = $gateway; - } - - - public function displayFormReact(): HttpResponse { - return ViewHttpResponse::react("views/SampleForm.tsx", []); - } - - public function displayFormTwig(): HttpResponse { - return ViewHttpResponse::twig('sample_form.html.twig', []); - } - - private function submitForm(array $form, callable $response): HttpResponse { - return Control::runCheckedFrom($form, [ - "name" => [Validators::lenBetween(0, 32), Validators::name("Le nom ne peut contenir que des lettres, des chiffres et des accents")], - "description" => [Validators::lenBetween(0, 512)] - ], function (HttpRequest $req) use ($response) { - $description = htmlspecialchars($req["description"]); - $this->gateway->insert($req["name"], $description); - $results = ["results" => $this->gateway->listResults()]; - return call_user_func_array($response, [$results]); - }, false); - } - - public function submitFormTwig(array $form): HttpResponse { - return $this->submitForm($form, fn(array $results) => ViewHttpResponse::twig('display_results.html.twig', $results)); - } - - public function submitFormReact(array $form): HttpResponse { - return $this->submitForm($form, fn(array $results) => ViewHttpResponse::react('views/DisplayResults.tsx', $results)); - } -} \ No newline at end of file diff --git a/src/Gateway/FormResultGateway.php b/src/Gateway/FormResultGateway.php deleted file mode 100644 index fe0c601..0000000 --- a/src/Gateway/FormResultGateway.php +++ /dev/null @@ -1,33 +0,0 @@ -con = $con; - } - - - function insert(string $username, string $description) { - $this->con->exec( - "INSERT INTO FormEntries VALUES (:name, :description)", - [ - ":name" => [$username, PDO::PARAM_STR], - "description" => [$description, PDO::PARAM_STR] - ] - ); - } - - function listResults(): array { - return $this->con->fetch("SELECT * FROM FormEntries", []); - } -} \ No newline at end of file diff --git a/src/Views/home.twig b/src/Views/home.twig index 8b5ce94..b65af07 100644 --- a/src/Views/home.twig +++ b/src/Views/home.twig @@ -8,6 +8,6 @@ Document -

Test

+

Page Home à faire

\ No newline at end of file diff --git a/src/Views/sample_form.html.twig b/src/Views/sample_form.html.twig deleted file mode 100644 index bcb958e..0000000 --- a/src/Views/sample_form.html.twig +++ /dev/null @@ -1,20 +0,0 @@ - - - - - Twig view - - - -

Hello, this is a sample form made in Twig !

- -
- - - - - -
- - - \ No newline at end of file From 738d2f94107a10e94f87f4b060a4a98b78c8c99d Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Fri, 17 Nov 2023 14:34:38 +0100 Subject: [PATCH 15/75] prepare front to persistable tactics --- front/Fetcher.ts | 13 +++ front/components/editor/BasketCourt.tsx | 37 ++++----- front/components/editor/CourtPlayer.tsx | 60 ++++++++------ front/{data => tactic}/Player.ts | 2 +- front/tactic/Tactic.ts | 11 +++ front/{data => tactic}/Team.tsx | 0 front/views/Editor.tsx | 93 ++++++++++++++-------- public/api/index.php | 3 +- src/Api/Controller/APITacticController.php | 9 +++ 9 files changed, 149 insertions(+), 79 deletions(-) create mode 100644 front/Fetcher.ts rename front/{data => tactic}/Player.ts (95%) create mode 100644 front/tactic/Tactic.ts rename front/{data => tactic}/Team.tsx (100%) diff --git a/front/Fetcher.ts b/front/Fetcher.ts new file mode 100644 index 0000000..59b15c8 --- /dev/null +++ b/front/Fetcher.ts @@ -0,0 +1,13 @@ +import {API} from "./Constants"; + + +export function fetchAPI(url: string, payload: object, method = "POST"): Promise { + return fetch(`${API}/${url}`, { + method, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) +} diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 9f4cb5d..7eab38c 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,26 +1,27 @@ -import CourtSvg from "../../assets/basketball_court.svg?react" -import "../../style/basket_court.css" -import { useRef } from "react" -import CourtPlayer from "./CourtPlayer" -import { Player } from "../../data/Player" +import CourtSvg from '../../assets/basketball_court.svg?react'; +import '../../style/basket_court.css'; +import {useRef} from "react"; +import CourtPlayer from "./CourtPlayer"; +import {Player} from "../../tactic/Player"; export interface BasketCourtProps { - players: Player[] - onPlayerRemove: (p: Player) => void + players: Player[], + onPlayerRemove: (p: Player) => void, + onPlayerChange: (p: Player) => void } -export function BasketCourt({ players, onPlayerRemove }: BasketCourtProps) { +export function BasketCourt({players, onPlayerRemove, onPlayerChange}: BasketCourtProps) { + const divRef = useRef(null); + return ( -
- - {players.map((player) => { - return ( - onPlayerRemove(player)} - /> - ) +
+ + {players.map(player => { + return onPlayerRemove(player)} + /> })}
) diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index 9b08e7b..b8ff341 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,40 +1,52 @@ -import { useRef } from "react" -import "../../style/player.css" -import RemoveIcon from "../../assets/icon/remove.svg?react" -import Draggable from "react-draggable" -import { PlayerPiece } from "./PlayerPiece" -import { Player } from "../../data/Player" + +import {useRef} from "react"; +import "../../style/player.css"; +import RemoveIcon from "../../assets/icon/remove.svg?react"; +import Draggable from "react-draggable"; +import {PlayerPiece} from "./PlayerPiece"; +import {Player} from "../../tactic/Player"; export interface PlayerProps { - player: Player + player: Player, + onChange: (p: Player) => void, onRemove: () => void } /** * A player that is placed on the court, which can be selected, and moved in the associated bounds * */ -export default function CourtPlayer({ player, onRemove }: PlayerProps) { - const ref = useRef(null) + +export default function CourtPlayer({player, onChange, onRemove}: PlayerProps) { const x = player.rightRatio const y = player.bottomRatio return ( - -
-
{ - if (e.key == "Delete") onRemove() - }}> + onChange({ + id: player.id, + rightRatio: player.rightRatio, + bottomRatio: player.bottomRatio, + team: player.team, + role: player.role + })} + > +
+ +
{ + if (e.key == "Delete") + onRemove() + }}>
Promise, + onNameChange: (name: string) => Promise +} + /** * information about a player that is into a rack */ @@ -21,8 +29,21 @@ interface RackedPlayer { key: string } -export default function Editor({ id, name }: { id: number; name: string }) { - const [style, setStyle] = useState({}) +export default function Editor({tactic}: { tactic: Tactic }) { + return ( + fetchAPI(`tactic/${tactic.id}/save`, {content}) + .then((r) => r.ok) + )} + onNameChange={(name: string) => ( + fetchAPI(`tactic/${tactic.id}/edit/name`, {name}) + .then((r) => r.ok) + )}/> +} + +function EditorView({tactic: {name, content}, onContentChange, onNameChange}: EditorViewProps) { + const [style, setStyle] = useState({}); + const positions = ["1", "2", "3", "4", "5"] const [allies, setAllies] = useState( positions.map((key) => ({ team: Team.Allies, key })), @@ -31,8 +52,17 @@ export default function Editor({ id, name }: { id: number; name: string }) { positions.map((key) => ({ team: Team.Opponents, key })), ) - const [players, setPlayers] = useState([]) - const courtDivContentRef = useRef(null) + + const [players, setPlayers] = useState(content.players); + const courtDivContentRef = useRef(null); + + useEffect(() => { + onContentChange({players}) + .then(success => { + if (!success) + alert("error when saving changes.") + }) + }, [players]) const canDetach = (ref: HTMLDivElement) => { const refBounds = ref.getBoundingClientRect() @@ -75,28 +105,15 @@ export default function Editor({ id, name }: { id: number; name: string }) {
LEFT
- { - fetch(`${API}/tactic/${id}/edit/name`, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: new_name, - }), - }).then((response) => { - if (response.ok) { - setStyle({}) - } else { - setStyle(ERROR_STYLE) - } - }) - }} - /> + { + onNameChange(new_name).then(success => { + if (success) { + setStyle({}) + } else { + setStyle(ERROR_STYLE) + } + }) + }}/>
RIGHT
@@ -126,6 +143,12 @@ export default function Editor({ id, name }: { id: number; name: string }) {
{ + setPlayers(players => { + const idx = players.indexOf(player) + return players.toSpliced(idx, 1, player) + }) + }} onPlayerRemove={(player) => { setPlayers((players) => { const idx = players.indexOf(player) diff --git a/public/api/index.php b/public/api/index.php index fd39bfa..8e1b9ef 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -29,8 +29,9 @@ function getRoutes(): AltoRouter { $router = new AltoRouter(); $router->setBasePath(get_public_path(__DIR__)); - $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); $router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize())); + $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); + $router->map("POST", "/tactic/[i:id]/save", Action::auth(fn(int $id) => getTacticController()->saveContent($id))); return $router; } diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index e8a1731..0edfbb3 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -45,4 +45,13 @@ class APITacticController { return HttpResponse::fromCode(HttpCodes::OK); }); } + + /** + * @param int $id + * @param Account $account + * @return HttpResponse + */ + public function saveContent(int $id, Account $account): HttpResponse { + return HttpResponse::fromCode(HttpCodes::OK); + } } From c53a1b024c6b026e2aec5c1ccfe3e6aa66ad477f Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Fri, 17 Nov 2023 14:34:38 +0100 Subject: [PATCH 16/75] apply suggestions --- front/components/Rack.tsx | 39 ++++++++-------- front/components/editor/BasketCourt.tsx | 38 ++++++--------- front/components/editor/CourtPlayer.tsx | 30 ++++++------ front/components/editor/PlayerPiece.tsx | 3 +- front/data/Player.ts | 10 ++-- front/data/Team.tsx | 4 ++ front/style/basket_court.css | 1 + front/style/editor.css | 5 ++ front/style/player.css | 10 ++-- front/views/Editor.tsx | 62 ++++++++++++++++--------- tsconfig.json | 2 +- vite.config.ts | 2 +- 12 files changed, 116 insertions(+), 90 deletions(-) create mode 100644 front/data/Team.tsx diff --git a/front/components/Rack.tsx b/front/components/Rack.tsx index ad8f354..09681e7 100644 --- a/front/components/Rack.tsx +++ b/front/components/Rack.tsx @@ -1,40 +1,39 @@ -import {Dispatch, ReactElement, RefObject, SetStateAction, useRef} from "react"; +import {ReactElement, useRef} from "react"; import Draggable from "react-draggable"; -export interface RackInput { +export interface RackProps { id: string, - objects: [ReactElement[], Dispatch>], - canDetach: (ref: RefObject) => boolean, - onElementDetached: (ref: RefObject, el: ReactElement) => void, + objects: E[], + onChange: (objects: E[]) => void, + canDetach: (ref: HTMLDivElement) => boolean, + onElementDetached: (ref: HTMLDivElement, el: E) => void, + render: (e: E) => ReactElement, } -interface RackItemInput { - item: ReactElement, - onTryDetach: (ref: RefObject, el: ReactElement) => void +interface RackItemProps { + item: E, + onTryDetach: (ref: HTMLDivElement, el: E) => void, + render: (e: E) => ReactElement, } /** * A container of draggable objects * */ -export function Rack({id, objects, canDetach, onElementDetached}: RackInput) { - - const [rackObjects, setRackObjects] = objects - +export function Rack({id, objects, onChange, canDetach, onElementDetached, render}: RackProps) { return (
- {rackObjects.map(element => ( + {objects.map(element => ( { if (!canDetach(ref)) return - setRackObjects(objects => { - const index = objects.findIndex(o => o.key === element.key) - return objects.toSpliced(index, 1); - }) + const index = objects.findIndex(o => o.key === element.key) + onChange(objects.toSpliced(index, 1)) onElementDetached(ref, element) }}/> @@ -43,16 +42,16 @@ export function Rack({id, objects, canDetach, onElementDetached}: RackInput) { ) } -function RackItem({item, onTryDetach}: RackItemInput) { +function RackItem({item, onTryDetach, render}: RackItemProps) { const divRef = useRef(null); return ( onTryDetach(divRef, item)}> + onStop={() => onTryDetach(divRef.current!, item)}>
- {item} + {render(item)}
) diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 0819633..b0b0eda 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,35 +1,27 @@ -import CourtSvg from '../../assets/basketball_court.svg'; +import CourtSvg from '../../assets/basketball_court.svg?react'; import '../../style/basket_court.css'; -import {MouseEvent, ReactElement, useEffect, useRef, useState} from "react"; +import {useRef} from "react"; import CourtPlayer from "./CourtPlayer"; import {Player} from "../../data/Player"; -export function BasketCourt({players, onPlayerRemove}: { players: Player[], onPlayerRemove: (Player) => void }) { - const [courtPlayers, setCourtPlayers] = useState([]) - const divRef = useRef(null); +export interface BasketCourtProps { + players: Player[], + onPlayerRemove: (p: Player) => void, +} - useEffect(() => { - const bounds = divRef.current!.getBoundingClientRect(); - setCourtPlayers(players.map(player => { - return ( - onPlayerRemove(player)} - /> - ) - })) - }, [players, divRef]); +export function BasketCourt({players, onPlayerRemove}: BasketCourtProps) { + const divRef = useRef(null); return ( -
+
- {courtPlayers} + {players.map(player => { + return onPlayerRemove(player)} + /> + })}
) } - diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index 08323f4..b6b64de 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,35 +1,37 @@ -import React, {useRef} from "react"; +import {useRef} from "react"; import "../../style/player.css"; -import RemoveIcon from "../../assets/icon/remove.svg"; -import Draggable, {DraggableBounds} from "react-draggable"; +import RemoveIcon from "../../assets/icon/remove.svg?react"; +import Draggable from "react-draggable"; import {PlayerPiece} from "./PlayerPiece"; +import {Player} from "../../data/Player"; -export interface PlayerOptions { - pos: string, - team: string, - x: number, - y: number, - bounds: DraggableBounds, +export interface PlayerProps { + player: Player, onRemove: () => void } /** * A player that is placed on the court, which can be selected, and moved in the associated bounds * */ -export default function CourtPlayer({pos, team, x, y, bounds, onRemove}: PlayerOptions) { +export default function CourtPlayer({player, onRemove}: PlayerProps) { const ref = useRef(null); + + const x = player.rightRatio; + const y = player.bottomRatio; + return (
onRemove()}/> + onClick={onRemove}/>
- +
diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx index b5cc41f..83e7dfc 100644 --- a/front/components/editor/PlayerPiece.tsx +++ b/front/components/editor/PlayerPiece.tsx @@ -1,8 +1,9 @@ import React from "react"; import '../../style/player.css' +import {Team} from "../../data/Team"; -export function PlayerPiece({team, text}: { team: string, text: string }) { +export function PlayerPiece({team, text}: { team: Team, text: string }) { return (

{text}

diff --git a/front/data/Player.ts b/front/data/Player.ts index a27e643..af88c1c 100644 --- a/front/data/Player.ts +++ b/front/data/Player.ts @@ -1,3 +1,5 @@ +import {Team} from "./Team"; + export interface Player { /** * unique identifier of the player. @@ -8,20 +10,20 @@ export interface Player { /** * the player's team * */ - team: "allies" | "opponents", + team: Team, /** * player's position * */ - position: string, + role: string, /** * Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) */ - bottom_percentage: number + bottomRatio: number /** * Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle) */ - right_percentage: number, + rightRatio: number, } \ No newline at end of file diff --git a/front/data/Team.tsx b/front/data/Team.tsx new file mode 100644 index 0000000..ea4c384 --- /dev/null +++ b/front/data/Team.tsx @@ -0,0 +1,4 @@ +export enum Team { + Allies = "allies", + Opponents = "opponents" +} \ No newline at end of file diff --git a/front/style/basket_court.css b/front/style/basket_court.css index 920512b..a5bc688 100644 --- a/front/style/basket_court.css +++ b/front/style/basket_court.css @@ -3,6 +3,7 @@ #court-container { display: flex; + background-color: var(--main-color); } diff --git a/front/style/editor.css b/front/style/editor.css index e2d38c9..3aad26c 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -52,3 +52,8 @@ #court-div-bounds { width: 60%; } + + +.react-draggable { + z-index: 2; +} \ No newline at end of file diff --git a/front/style/player.css b/front/style/player.css index ebd0462..264b479 100644 --- a/front/style/player.css +++ b/front/style/player.css @@ -9,15 +9,11 @@ on the court. } .player-content { - /*apply a translation to center the player piece when placed*/ - transform: translate(-29%, -46%); - display: flex; flex-direction: column; align-content: center; align-items: center; outline: none; - } .player-piece { @@ -44,14 +40,18 @@ on the court. .player-selection-tab { display: flex; + + position: absolute; margin-bottom: 10%; justify-content: center; visibility: hidden; + + width: 100%; + transform: translateY(-20px); } .player-selection-tab-remove { pointer-events: all; - width: 25%; height: 25%; } diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 8636cad..dba6dc2 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -7,27 +7,36 @@ import {BasketCourt} from "../components/editor/BasketCourt"; import {Rack} from "../components/Rack"; import {PlayerPiece} from "../components/editor/PlayerPiece"; import {Player} from "../data/Player"; +import {Team} from "../data/Team"; const ERROR_STYLE: CSSProperties = { borderColor: "red" } +/** + * information about a player that is into a rack + */ +interface RackedPlayer { + team: Team, + key: string, +} + export default function Editor({id, name}: { id: number, name: string }) { const [style, setStyle] = useState({}); const positions = ["1", "2", "3", "4", "5"] const [allies, setAllies] = useState( - positions.map(pos => ) + positions.map(key => ({team: Team.Allies, key})) ) const [opponents, setOpponents] = useState( - positions.map(pos => ) + positions.map(key => ({team: Team.Opponents, key})) ) const [players, setPlayers] = useState([]); const courtDivContentRef = useRef(null); - const canDetach = (ref: RefObject) => { - const refBounds = ref.current!.getBoundingClientRect(); + const canDetach = (ref: HTMLDivElement) => { + const refBounds = ref.getBoundingClientRect(); const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); // check if refBounds overlaps courtBounds @@ -39,23 +48,23 @@ export default function Editor({id, name}: { id: number, name: string }) { ); } - const onElementDetach = (ref: RefObject, element: ReactElement) => { - const refBounds = ref.current!.getBoundingClientRect(); + const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => { + const refBounds = ref.getBoundingClientRect(); const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); const relativeXPixels = refBounds.x - courtBounds.x; const relativeYPixels = refBounds.y - courtBounds.y; - const xPercent = relativeXPixels / courtBounds.width; - const yPercent = relativeYPixels / courtBounds.height; + const xRatio = relativeXPixels / courtBounds.width; + const yRatio = relativeYPixels / courtBounds.height; setPlayers(players => { return [...players, { id: players.length, - team: element.props.team, - position: element.props.text, - right_percentage: xPercent, - bottom_percentage: yPercent + team: element.team, + role: element.key, + rightRatio: xRatio, + bottomRatio: yRatio }] }) } @@ -87,13 +96,17 @@ export default function Editor({id, name}: { id: number, name: string }) {
+ onElementDetached={onPieceDetach} + render={({team, key}) => }/> + onElementDetached={onPieceDetach} + render={({team, key}) => }/>
@@ -104,16 +117,23 @@ export default function Editor({id, name}: { id: number, name: string }) { const idx = players.indexOf(player) return players.toSpliced(idx, 1) }) - const piece = switch (player.team) { - case "opponents": + case Team.Opponents: setOpponents(opponents => ( - [...opponents, piece] + [...opponents, { + team: player.team, + pos: player.role, + key: player.role + }] )) break - case "allies": + case Team.Allies: setAllies(allies => ( - [...allies, piece] + [...allies, { + team: player.team, + pos: player.role, + key: player.role + }] )) } }}/> diff --git a/tsconfig.json b/tsconfig.json index 9da1fb5..d01f3cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "dom.iterable", "esnext" ], - "types": ["vite/client"], + "types": ["vite/client", "vite-plugin-svgr/client"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, diff --git a/vite.config.ts b/vite.config.ts index bb04351..4ff1dc5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -41,7 +41,7 @@ export default defineConfig({ relativeCSSInjection: true, }), svgr({ - include: "**/*.svg" + include: "**/*.svg?react" }) ] }) From 9bc03c099b33a3b4849f6318f6620425e9d48eaf Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Sun, 19 Nov 2023 22:42:30 +0100 Subject: [PATCH 17/75] WIP did a few little changes, add colorpicker for the creation of the team, still have to make changes in regard to it --- sql/setup-tables.sql | 16 +++++++++------- src/Controller/TeamController.php | 15 +++++++++------ src/Data/Member.php | 13 +------------ src/Gateway/TeamGateway.php | 7 +++++++ src/Model/TeamModel.php | 12 +++++++----- src/Views/display_teams.html.twig | 4 ++-- src/Views/insert_team.html.twig | 4 ++-- 7 files changed, 37 insertions(+), 34 deletions(-) diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 437649c..c2e8ec4 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -1,6 +1,9 @@ -- drop tables here DROP TABLE IF EXISTS FormEntries; DROP TABLE IF EXISTS TacticInfo; +DROP TABLE IF EXISTS Team; +DROP TABLE IF EXISTS Member; +DROP TABLE IF EXISTS Participate; CREATE TABLE FormEntries(name varchar, description varchar); @@ -12,15 +15,14 @@ CREATE TABLE Team( secondColor numeric ); -CREATE TABLE Participate( - idTeam numeric FOREIGN KEY REFERENCES Team(id), - idMember numeric FOREIGN KEY REFERENCES Member(id), - role char CHECK (role IN ('C','P')) +CREATE TABLE Member( + id integer PRIMARY KEY AUTOINCREMENT ); -CREATE TABLE Member( - id integer PRIMARY KEY AUTOINCREMENT, - email varchar, +CREATE TABLE Participate( + idTeam integer FOREIGN KEY REFERENCES Team(id), + idMember integer FOREIGN KEY REFERENCES Member(id), + role char CHECK (role IN ('C','P')) ); CREATE TABLE TacticInfo( diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index ffbcb8e..e2f0b89 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -9,9 +9,6 @@ use App\Model\TeamModel; use App\Validation\FieldValidationFail; use App\Validation\Validators; use \Twig\Environment; -use Twig\Error\LoaderError; -use Twig\Error\RuntimeError; -use Twig\Error\SyntaxError; class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ { @@ -32,7 +29,9 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ } public function submitTeam(array $request): HttpResponse { - + $request['mainColor'] = intval($request['mainColor']); + $request['secondColor'] = intval($request['secondColor']); + var_dump($request['secondColor']); $errors = []; $request = HttpRequest::from($request, $errors, [ "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], @@ -49,8 +48,8 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ } return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]); } - $this->model->createTeam($request['name'], $request['picture'], intval($request['mainColor']), intval($request['secondColor'])); - return ViewHttpResponse::twig('sample_form.html.twig', []); /*todo appeler une vue qui display la team au lieu de ça*/ + $id = $this->model->createTeam($request['name'], $request['picture'], $request['mainColor'], $request['secondColor']); + return $this->displayTeam($id); } public function displayListTeamByName(): HttpResponse { @@ -62,14 +61,18 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ $request = HttpRequest::from($request, $errors, [ "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()] ]); + if (!empty($errors) && $errors[0] instanceof FieldValidationFail) { $badField = $errors[0]->getFieldName(); return ViewHttpResponse::twig('list_team_by_name.html.twig', ['bad_field' => $badField]); } + $results = $this->model->listByName($request['name']); + if (empty($results)) { return ViewHttpResponse::twig('display_teams.html.twig', []); } + return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $results]); } diff --git a/src/Data/Member.php b/src/Data/Member.php index 9bf0b52..bfdb7a8 100755 --- a/src/Data/Member.php +++ b/src/Data/Member.php @@ -11,7 +11,6 @@ class Member { */ private int $userId; - private string $email; /** * @var MemberRole the member's role */ @@ -19,12 +18,10 @@ class Member { /** * @param int $userId - * @param string $email * @param MemberRole $role */ - public function __construct(int $userId, string $email, MemberRole $role) { + public function __construct(int $userId, MemberRole $role) { $this->userId = $userId; - $this->email = $email; $this->role = $role; } @@ -42,12 +39,4 @@ class Member { public function getRole(): MemberRole { return $this->role; } - - /** - * @return string - */ - public function getEmail(): string { - return $this->email; - } - } \ No newline at end of file diff --git a/src/Gateway/TeamGateway.php b/src/Gateway/TeamGateway.php index 1540c73..0f8fa74 100644 --- a/src/Gateway/TeamGateway.php +++ b/src/Gateway/TeamGateway.php @@ -24,6 +24,13 @@ class TeamGateway { ); } + public function getIdOfLastInsertion(): array{ + return $this->con->fetch( + "SELECT last_insert_rowid() as id", + [] + ); + } + public function listByName(string $name): array { return $this->con->fetch( "SELECT id,name,picture,mainColor,secondColor FROM Team WHERE name LIKE '%' || :name || '%'", diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php index 8c5c8f5..17b02dd 100644 --- a/src/Model/TeamModel.php +++ b/src/Model/TeamModel.php @@ -6,6 +6,7 @@ use App\Gateway\TeamGateway; use App\Data\Team; use App\Data\Member; use App\Data\MemberRole; +use http\Url; /** * @@ -21,15 +22,17 @@ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) po $this->gateway = $gateway; } - public function createTeam(string $name, string $picture, int $mainColor, int $secondColor) { + public function createTeam(string $name, string $picture, int $mainColor, int $secondColor): int{ $this->gateway->insert($name, $picture, $mainColor, $secondColor); + return (int) $this->gateway->getIdOfLastInsertion()['id']; } public function listByName(string $name): array { $teams = []; $results = $this->gateway->listByName($name); foreach ($results as $row) { - $teams[] = new Team($row['id'], $row['name'], $row['picture'], $row['mainColor'], $row['secondColor']); + $url = new Url($row['picture']); + $teams[] = new Team($row['id'], $row['name'], $url, $row['mainColor'], $row['secondColor']); } return $teams; } @@ -44,10 +47,9 @@ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) po } else { $role = 2; } - $members[] = new Member($row['id'], $row['email'], $role); + $members[] = new Member($row['id'], $role); } - $team = new Team($result['id'], $result['name'], $result['picture'], $result['mainColor'], $result['secondColor'], $members); - return $team; + return new Team($result['id'], $result['name'], $result['picture'], $result['mainColor'], $result['secondColor'], $members); } } \ No newline at end of file diff --git a/src/Views/display_teams.html.twig b/src/Views/display_teams.html.twig index d0c7c22..e4941e4 100644 --- a/src/Views/display_teams.html.twig +++ b/src/Views/display_teams.html.twig @@ -22,8 +22,8 @@
{% else %} {% for t in teams %} -
-

Nom de l'équipe: {{ t.name }}

+
+

Nom de l'équipe : {{ t.name }}

picture : {{ t.picture }}

{% endfor %} diff --git a/src/Views/insert_team.html.twig b/src/Views/insert_team.html.twig index ba6ffac..0cc85dd 100644 --- a/src/Views/insert_team.html.twig +++ b/src/Views/insert_team.html.twig @@ -71,9 +71,9 @@ - + - +
From a18014d4c37affbe38d844a8cc63e897ce63da0a Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Mon, 20 Nov 2023 08:25:40 +0100 Subject: [PATCH 18/75] apply suggestions --- front/components/editor/BasketCourt.tsx | 4 +--- front/components/editor/CourtPlayer.tsx | 6 +----- front/views/Editor.tsx | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index b0b0eda..7e839a8 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -10,10 +10,8 @@ export interface BasketCourtProps { } export function BasketCourt({players, onPlayerRemove}: BasketCourtProps) { - const divRef = useRef(null); - return ( -
+
{players.map(player => { return (null); - const x = player.rightRatio; const y = player.bottomRatio; return ( -
Date: Mon, 20 Nov 2023 09:20:26 +0100 Subject: [PATCH 19/75] persist player position --- front/Utils.ts | 9 +++ front/components/editor/BasketCourt.tsx | 1 + front/components/editor/CourtPlayer.tsx | 82 +++++++++++++++---------- front/views/Editor.tsx | 29 ++++----- 4 files changed, 72 insertions(+), 49 deletions(-) create mode 100644 front/Utils.ts diff --git a/front/Utils.ts b/front/Utils.ts new file mode 100644 index 0000000..5b604a0 --- /dev/null +++ b/front/Utils.ts @@ -0,0 +1,9 @@ +export function calculateRatio(it: { x: number, y: number }, parent: DOMRect): { x: number, y: number } { + const relativeXPixels = it.x - parent.x; + const relativeYPixels = it.y - parent.y; + + const xRatio = relativeXPixels / parent.width; + const yRatio = relativeYPixels / parent.height; + + return {x: xRatio, y: yRatio} +} diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 7eab38c..4ae8c5f 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -21,6 +21,7 @@ export function BasketCourt({players, onPlayerRemove, onPlayerChange}: BasketCou player={player} onChange={onPlayerChange} onRemove={() => onPlayerRemove(player)} + parentRef={divRef} /> })}
diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index b8ff341..a94f957 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,52 +1,70 @@ -import {useRef} from "react"; -import "../../style/player.css"; -import RemoveIcon from "../../assets/icon/remove.svg?react"; -import Draggable from "react-draggable"; -import {PlayerPiece} from "./PlayerPiece"; -import {Player} from "../../tactic/Player"; +import { MutableRefObject, useEffect, useRef, useState } from "react" +import "../../style/player.css" +import RemoveIcon from "../../assets/icon/remove.svg?react" +import Draggable from "react-draggable" +import { PlayerPiece } from "./PlayerPiece" +import { Player } from "../../tactic/Player" +import { calculateRatio } from "../../Utils" export interface PlayerProps { player: Player, onChange: (p: Player) => void, onRemove: () => void + parentRef: MutableRefObject } /** * A player that is placed on the court, which can be selected, and moved in the associated bounds * */ +export default function CourtPlayer({ + player, + onChange, + onRemove, + parentRef, +}: PlayerProps) { + const pieceRef = useRef(null) -export default function CourtPlayer({player, onChange, onRemove}: PlayerProps) { - - const x = player.rightRatio - const y = player.bottomRatio + const [x, setX] = useState(player.rightRatio) + const [y, setY] = useState(player.bottomRatio) return ( onChange({ - id: player.id, - rightRatio: player.rightRatio, - bottomRatio: player.bottomRatio, - team: player.team, - role: player.role - })} - > -
- -
{ - if (e.key == "Delete") - onRemove() - }}> + position={{ x, y }} + onStop={() => { + const pieceBounds = pieceRef.current!.getBoundingClientRect() + const parentBounds = parentRef.current!.getBoundingClientRect() + + const { x, y } = calculateRatio(pieceBounds, parentBounds) + + setX(x) + setY(y) + + onChange({ + id: player.id, + rightRatio: x, + bottomRatio: y, + team: player.team, + role: player.role, + }) + }}> +
+
{ + if (e.key == "Delete") onRemove() + }}>
{ - return [ - ...players, - { - id: players.length, - team: element.team, - role: element.key, - rightRatio: xRatio, - bottomRatio: yRatio, - }, - ] + + const {x, y} = calculateRatio(refBounds, courtBounds) + + setPlayers(players => { + return [...players, { + id: players.length, + team: element.team, + role: element.key, + rightRatio: x, + bottomRatio: y + }] }) } From 46862bb7cd91b3e3e060cfb1f5f06d0749dd721b Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Mon, 20 Nov 2023 09:53:53 +0100 Subject: [PATCH 20/75] WIP fixed some bugs, still got two regarding /team/id and /team/list --- sql/setup-tables.sql | 8 +++++--- src/Controller/TeamController.php | 14 +++++++------- src/Data/MemberRole.php | 13 +++++++++++-- src/Gateway/TeamGateway.php | 16 +++++++++------- src/Model/TeamModel.php | 7 ++++--- 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index c2e8ec4..1366a87 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -20,9 +20,11 @@ CREATE TABLE Member( ); CREATE TABLE Participate( - idTeam integer FOREIGN KEY REFERENCES Team(id), - idMember integer FOREIGN KEY REFERENCES Member(id), - role char CHECK (role IN ('C','P')) + idTeam integer, + idMember integer, + role char(1) CHECK (role IN ('C','P')), + FOREIGN KEY (idTeam) REFERENCES Team(id), + FOREIGN KEY (idMember) REFERENCES Member(id) ); CREATE TABLE TacticInfo( diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index e2f0b89..30a6ab6 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -29,10 +29,11 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ } public function submitTeam(array $request): HttpResponse { - $request['mainColor'] = intval($request['mainColor']); - $request['secondColor'] = intval($request['secondColor']); - var_dump($request['secondColor']); + $errors = []; + $request['mainColor'] = hexdec($request['mainColor']); + $request['secondColor'] = hexdec($request['secondColor']); + $request = HttpRequest::from($request, $errors, [ "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], "mainColor" => [Validators::isInteger(), Validators::isIntInRange(0, 0xffffff)], @@ -48,8 +49,7 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ } return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]); } - $id = $this->model->createTeam($request['name'], $request['picture'], $request['mainColor'], $request['secondColor']); - return $this->displayTeam($id); + return $this->displayTeam($this->model->createTeam($request['name'], $request['picture'], $request['mainColor'], $request['secondColor'])); } public function displayListTeamByName(): HttpResponse { @@ -76,9 +76,9 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $results]); } - public function displayTeam(int $id): HttpResponse{ + public function displayTeam(int $id): HttpResponse { $result = $this->model->displayTeam($id); - return ViewHttpResponse::twig('display_team.html.twig',['team' => $result]); + return ViewHttpResponse::twig('display_team.html.twig', ['team' => $result]); } } diff --git a/src/Data/MemberRole.php b/src/Data/MemberRole.php index 05d746d..bd8add3 100755 --- a/src/Data/MemberRole.php +++ b/src/Data/MemberRole.php @@ -11,8 +11,9 @@ use http\Exception\InvalidArgumentException; * encapsulates an integer value and use it as an enumeration discriminant */ final class MemberRole { - private const ROLE_PLAYER = 0; - private const ROLE_COACH = 1; + + public const ROLE_PLAYER = 0; + public const ROLE_COACH = 1; private const MIN = self::ROLE_PLAYER; private const MAX = self::ROLE_COACH; @@ -25,6 +26,14 @@ final class MemberRole { $this->value = $val; } + public static function player(){ + return new MemberRole(MemberRole::ROLE_PLAYER); + } + + public static function coach(){ + return new MemberRole(MemberRole::ROLE_COACH); + } + private function isValid(int $val): bool { return ($val <= self::MAX and $val >= self::MIN); } diff --git a/src/Gateway/TeamGateway.php b/src/Gateway/TeamGateway.php index 0f8fa74..4bb1518 100644 --- a/src/Gateway/TeamGateway.php +++ b/src/Gateway/TeamGateway.php @@ -24,13 +24,6 @@ class TeamGateway { ); } - public function getIdOfLastInsertion(): array{ - return $this->con->fetch( - "SELECT last_insert_rowid() as id", - [] - ); - } - public function listByName(string $name): array { return $this->con->fetch( "SELECT id,name,picture,mainColor,secondColor FROM Team WHERE name LIKE '%' || :name || '%'", @@ -49,6 +42,15 @@ class TeamGateway { ); } + public function getIdTeamByName(string $name): array{ + return $this->con->fetch( + "SELECT id FROM Team WHERE name = :name", + [ + ":name" => [$name, PDO::PARAM_STR] + ] + ); + } + public function getMembersById($id):array{ return $this->con->fetch( "SELECT p.role,m.email,m.id FROM Member m,Team t,Participate p WHERE t.id = :id AND p.idTeam = t.id AND p.idMember = m.id", diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php index 17b02dd..1dd57a0 100644 --- a/src/Model/TeamModel.php +++ b/src/Model/TeamModel.php @@ -24,7 +24,8 @@ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) po public function createTeam(string $name, string $picture, int $mainColor, int $secondColor): int{ $this->gateway->insert($name, $picture, $mainColor, $secondColor); - return (int) $this->gateway->getIdOfLastInsertion()['id']; + $result = $this->gateway->getIdTeamByName($name); + return intval($result[0]['id']); } public function listByName(string $name): array { @@ -43,9 +44,9 @@ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) po $resultMembers = $this->gateway->getMembersById($id); foreach ($resultMembers as $row) { if ($row['role'] == 'C') { - $role = 1; + $role = MemberRole::coach(); } else { - $role = 2; + $role = MemberRole::player(); } $members[] = new Member($row['id'], $role); } From 6731b02c6c25030f9ad78b8eb1facd1a964553e8 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Mon, 20 Nov 2023 09:56:05 +0100 Subject: [PATCH 21/75] add save state information in topbar --- Documentation/models.puml | 2 + front/Fetcher.ts | 15 +- front/Utils.ts | 15 +- front/components/TitleInput.tsx | 2 +- front/components/editor/BasketCourt.tsx | 43 ++--- front/components/editor/CourtPlayer.tsx | 9 +- front/components/editor/PlayerPiece.tsx | 2 +- front/components/editor/SavingState.tsx | 27 ++++ front/style/editor.css | 34 +++- front/style/title_input.css | 4 +- front/tactic/Tactic.ts | 8 +- front/views/Editor.tsx | 173 ++++++++++++++------- public/api/index.php | 2 +- sql/setup-tables.sql | 18 ++- src/Api/Controller/APITacticController.php | 7 +- src/App/Controller/EditorController.php | 7 +- src/Core/Data/TacticInfo.php | 19 ++- src/Core/Gateway/TacticInfoGateway.php | 22 ++- src/Core/Model/TacticModel.php | 9 +- 19 files changed, 292 insertions(+), 126 deletions(-) create mode 100644 front/components/editor/SavingState.tsx diff --git a/Documentation/models.puml b/Documentation/models.puml index c0629d4..86ca699 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -5,11 +5,13 @@ class TacticInfo { - name: string - creationDate: string - ownerId: string + - content: string + getId(): int + getOwnerId(): int + getCreationTimestamp(): int + getName(): string + + getContent(): string } class Account { diff --git a/front/Fetcher.ts b/front/Fetcher.ts index 59b15c8..7c81fdd 100644 --- a/front/Fetcher.ts +++ b/front/Fetcher.ts @@ -1,13 +1,16 @@ -import {API} from "./Constants"; +import { API } from "./Constants" - -export function fetchAPI(url: string, payload: object, method = "POST"): Promise { +export function fetchAPI( + url: string, + payload: object, + method = "POST", +): Promise { return fetch(`${API}/${url}`, { method, headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' + Accept: "application/json", + "Content-Type": "application/json", }, - body: JSON.stringify(payload) + body: JSON.stringify(payload), }) } diff --git a/front/Utils.ts b/front/Utils.ts index 5b604a0..523d813 100644 --- a/front/Utils.ts +++ b/front/Utils.ts @@ -1,9 +1,12 @@ -export function calculateRatio(it: { x: number, y: number }, parent: DOMRect): { x: number, y: number } { - const relativeXPixels = it.x - parent.x; - const relativeYPixels = it.y - parent.y; +export function calculateRatio( + it: { x: number; y: number }, + parent: DOMRect, +): { x: number; y: number } { + const relativeXPixels = it.x - parent.x + const relativeYPixels = it.y - parent.y - const xRatio = relativeXPixels / parent.width; - const yRatio = relativeYPixels / parent.height; + const xRatio = relativeXPixels / parent.width + const yRatio = relativeYPixels / parent.height - return {x: xRatio, y: yRatio} + return { x: xRatio, y: yRatio } } diff --git a/front/components/TitleInput.tsx b/front/components/TitleInput.tsx index 6e4acb0..8da1c65 100644 --- a/front/components/TitleInput.tsx +++ b/front/components/TitleInput.tsx @@ -17,7 +17,7 @@ export default function TitleInput({ return ( void, + players: Player[] + onPlayerRemove: (p: Player) => void onPlayerChange: (p: Player) => void } -export function BasketCourt({players, onPlayerRemove, onPlayerChange}: BasketCourtProps) { - const divRef = useRef(null); +export function BasketCourt({ + players, + onPlayerRemove, + onPlayerChange, +}: BasketCourtProps) { + const divRef = useRef(null) return ( -
- - {players.map(player => { - return onPlayerRemove(player)} - parentRef={divRef} - /> +
+ + {players.map((player) => { + return ( + onPlayerRemove(player)} + parentRef={divRef} + /> + ) })}
) diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index a94f957..9f0c9e4 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,5 +1,4 @@ - -import { MutableRefObject, useEffect, useRef, useState } from "react" +import { RefObject, useRef, useState } from "react" import "../../style/player.css" import RemoveIcon from "../../assets/icon/remove.svg?react" import Draggable from "react-draggable" @@ -8,10 +7,10 @@ import { Player } from "../../tactic/Player" import { calculateRatio } from "../../Utils" export interface PlayerProps { - player: Player, - onChange: (p: Player) => void, + player: Player + onChange: (p: Player) => void onRemove: () => void - parentRef: MutableRefObject + parentRef: RefObject } /** diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx index 08bf36d..69b38c2 100644 --- a/front/components/editor/PlayerPiece.tsx +++ b/front/components/editor/PlayerPiece.tsx @@ -1,6 +1,6 @@ import React from "react" import "../../style/player.css" -import { Team } from "../../data/Team" +import { Team } from "../../tactic/Team" export function PlayerPiece({ team, text }: { team: Team; text: string }) { return ( diff --git a/front/components/editor/SavingState.tsx b/front/components/editor/SavingState.tsx new file mode 100644 index 0000000..df03628 --- /dev/null +++ b/front/components/editor/SavingState.tsx @@ -0,0 +1,27 @@ +export interface SaveState { + className: string + message: string +} + +export class SaveStates { + static readonly Ok: SaveState = { + className: "save-state-ok", + message: "saved", + } + static readonly Saving: SaveState = { + className: "save-state-saving", + message: "saving...", + } + static readonly Err: SaveState = { + className: "save-state-error", + message: "could not save tactic.", + } +} + +export default function SavingState({ state }: { state: SaveState }) { + return ( +
+
{state.message}
+
+ ) +} diff --git a/front/style/editor.css b/front/style/editor.css index b586a36..d832fc3 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -9,9 +9,21 @@ flex-direction: column; } +#topbar-left { + width: 100%; + display: flex; +} + +#topbar-right { + width: 100%; + display: flex; + flex-direction: row-reverse; +} + #topbar-div { display: flex; background-color: var(--main-color); + margin-bottom: 3px; justify-content: space-between; align-items: stretch; @@ -22,8 +34,9 @@ justify-content: space-between; } -.title_input { +.title-input { width: 25ch; + align-self: center; } #edit-div { @@ -56,3 +69,22 @@ .react-draggable { z-index: 2; } + +.save-state { + display: flex; + align-items: center; + margin-left: 20%; + font-family: monospace; +} + +.save-state-error { + color: red; +} + +.save-state-ok { + color: green; +} + +.save-state-saving { + color: gray; +} diff --git a/front/style/title_input.css b/front/style/title_input.css index 1b6be10..6d28238 100644 --- a/front/style/title_input.css +++ b/front/style/title_input.css @@ -1,4 +1,4 @@ -.title_input { +.title-input { background: transparent; border-top: none; border-right: none; @@ -9,7 +9,7 @@ border-bottom-color: transparent; } -.title_input:focus { +.title-input:focus { outline: none; border-bottom-color: blueviolet; diff --git a/front/tactic/Tactic.ts b/front/tactic/Tactic.ts index cef5c2a..bb2cd37 100644 --- a/front/tactic/Tactic.ts +++ b/front/tactic/Tactic.ts @@ -1,11 +1,11 @@ -import {Player} from "./Player"; +import { Player } from "./Player" export interface Tactic { - id: number, - name: string, + id: number + name: string content: TacticContent } export interface TacticContent { players: Player[] -} \ No newline at end of file +} diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 61f1176..8f6483c 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -1,24 +1,25 @@ -import {CSSProperties, useEffect, useRef, useState} from "react"; -import "../style/editor.css"; -import TitleInput from "../components/TitleInput"; -import {BasketCourt} from "../components/editor/BasketCourt"; +import React, { CSSProperties, useEffect, useRef, useState } from "react" +import "../style/editor.css" +import TitleInput from "../components/TitleInput" +import { BasketCourt } from "../components/editor/BasketCourt" -import {Rack} from "../components/Rack"; -import {PlayerPiece} from "../components/editor/PlayerPiece"; +import { Rack } from "../components/Rack" +import { PlayerPiece } from "../components/editor/PlayerPiece" -import {Player} from "../tactic/Player"; -import {Tactic, TacticContent} from "../tactic/Tactic"; -import {fetchAPI} from "../Fetcher"; -import {Team} from "../tactic/Team"; -import {calculateRatio} from "../Utils"; +import { Player } from "../tactic/Player" +import { Tactic, TacticContent } from "../tactic/Tactic" +import { fetchAPI } from "../Fetcher" +import { Team } from "../tactic/Team" +import { calculateRatio } from "../Utils" +import SavingState, { SaveStates } from "../components/editor/SavingState" const ERROR_STYLE: CSSProperties = { borderColor: "red", } export interface EditorViewProps { - tactic: Tactic, - onContentChange: (tactic: TacticContent) => Promise, + tactic: Tactic + onContentChange: (tactic: TacticContent) => Promise onNameChange: (name: string) => Promise } @@ -30,39 +31,78 @@ interface RackedPlayer { key: string } -export default function Editor({tactic}: { tactic: Tactic }) { - return ( - fetchAPI(`tactic/${tactic.id}/save`, {content}) - .then((r) => r.ok) - )} - onNameChange={(name: string) => ( - fetchAPI(`tactic/${tactic.id}/edit/name`, {name}) - .then((r) => r.ok) - )}/> +export default function Editor({ + id, + name, + content, +}: { + id: number + name: string + content: string +}) { + return ( + + fetchAPI(`tactic/${id}/save`, { content }).then((r) => r.ok) + } + onNameChange={(name: string) => + fetchAPI(`tactic/${id}/edit/name`, { name }).then((r) => r.ok) + } + /> + ) } -function EditorView({tactic: {name, content}, onContentChange, onNameChange}: EditorViewProps) { - const [style, setStyle] = useState({}); +function EditorView({ + tactic: { name, content }, + onContentChange, + onNameChange, +}: EditorViewProps) { + const [style, setStyle] = useState({}) + const [saveState, setSaveState] = useState(SaveStates.Ok) const positions = ["1", "2", "3", "4", "5"] const [allies, setAllies] = useState( - positions.map((key) => ({ team: Team.Allies, key })), + positions + .filter( + (role) => + content.players.findIndex( + (p) => p.team == Team.Allies && p.role == role, + ) == -1, + ) + .map((key) => ({ team: Team.Allies, key })), ) const [opponents, setOpponents] = useState( - positions.map((key) => ({ team: Team.Opponents, key })), + positions + .filter( + (role) => + content.players.findIndex( + (p) => p.team == Team.Opponents && p.role == role, + ) == -1, + ) + .map((key) => ({ team: Team.Opponents, key })), ) + const [players, setPlayers] = useState(content.players) + const courtDivContentRef = useRef(null) - const [players, setPlayers] = useState(content.players); - const courtDivContentRef = useRef(null); - + // The didMount ref is used to store a boolean flag in order to avoid calling 'onChange' when the editor is first rendered. + const didMount = useRef(false) useEffect(() => { - onContentChange({players}) - .then(success => { - if (!success) - alert("error when saving changes.") + if (!didMount.current) { + didMount.current = true + return + } + setSaveState(SaveStates.Saving) + onContentChange({ players }) + .then((success) => { + if (success) { + setSaveState(SaveStates.Ok) + } else { + setSaveState(SaveStates.Err) + } }) + .catch(() => setSaveState(SaveStates.Err)) }, [players]) const canDetach = (ref: HTMLDivElement) => { @@ -82,34 +122,45 @@ function EditorView({tactic: {name, content}, onContentChange, onNameChange}: Ed const refBounds = ref.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect() - - const {x, y} = calculateRatio(refBounds, courtBounds) - - setPlayers(players => { - return [...players, { - id: players.length, - team: element.team, - role: element.key, - rightRatio: x, - bottomRatio: y - }] + const { x, y } = calculateRatio(refBounds, courtBounds) + + setPlayers((players) => { + return [ + ...players, + { + id: players.length, + team: element.team, + role: element.key, + rightRatio: x, + bottomRatio: y, + }, + ] }) } return (
-
LEFT
- { - onNameChange(new_name).then(success => { - if (success) { - setStyle({}) - } else { - setStyle(ERROR_STYLE) - } - }) - }}/> -
RIGHT
+
+ LEFT + +
+
+ { + onNameChange(new_name).then((success) => { + if (success) { + setStyle({}) + } else { + setStyle(ERROR_STYLE) + } + }) + }} + /> +
+
RIGHT
@@ -139,14 +190,18 @@ function EditorView({tactic: {name, content}, onContentChange, onNameChange}: Ed { - setPlayers(players => { - const idx = players.indexOf(player) + setPlayers((players) => { + const idx = players.findIndex( + (p) => p.id === player.id, + ) return players.toSpliced(idx, 1, player) }) }} onPlayerRemove={(player) => { setPlayers((players) => { - const idx = players.indexOf(player) + const idx = players.findIndex( + (p) => p.id === player.id, + ) return players.toSpliced(idx, 1) }) switch (player.team) { diff --git a/public/api/index.php b/public/api/index.php index 8e1b9ef..5734571 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -31,7 +31,7 @@ function getRoutes(): AltoRouter { $router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize())); $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); - $router->map("POST", "/tactic/[i:id]/save", Action::auth(fn(int $id) => getTacticController()->saveContent($id))); + $router->map("POST", "/tactic/[i:id]/save", Action::auth(fn(int $id, Account $acc) => getTacticController()->saveContent($id, $acc))); return $router; } diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 324fb39..5a2eecf 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -19,26 +19,32 @@ CREATE TABLE Tactic name varchar NOT NULL, creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, owner integer NOT NULL, + content varchar DEFAULT '{"players": []}' NOT NULL, FOREIGN KEY (owner) REFERENCES Account ); -CREATE TABLE FormEntries(name varchar, description varchar); +CREATE TABLE FormEntries +( + name varchar, + description varchar +); CREATE TABLE Team ( - id integer PRIMARY KEY AUTOINCREMENT, - name varchar, - picture varchar, + id integer PRIMARY KEY AUTOINCREMENT, + name varchar, + picture varchar, main_color varchar, second_color varchar ); -CREATE TABLE Member( +CREATE TABLE Member +( id_team integer, id_user integer, - role char(1) CHECK (role IN ('Coach', 'Player')), + role char(1) CHECK (role IN ('Coach', 'Player')), FOREIGN KEY (id_team) REFERENCES Team (id), FOREIGN KEY (id_user) REFERENCES User (id) ); diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 0edfbb3..606e49b 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -52,6 +52,11 @@ class APITacticController { * @return HttpResponse */ public function saveContent(int $id, Account $account): HttpResponse { - return HttpResponse::fromCode(HttpCodes::OK); + return Control::runChecked([ + "content" => [], + ], function (HttpRequest $req) use ($id) { + $this->model->updateContent($id, json_encode($req["content"])); + return HttpResponse::fromCode(HttpCodes::OK); + }); } } diff --git a/src/App/Controller/EditorController.php b/src/App/Controller/EditorController.php index bba3214..d2a2bc4 100644 --- a/src/App/Controller/EditorController.php +++ b/src/App/Controller/EditorController.php @@ -8,7 +8,6 @@ use IQBall\App\ViewHttpResponse; use IQBall\Core\Data\TacticInfo; use IQBall\Core\Http\HttpCodes; use IQBall\Core\Model\TacticModel; -use IQBall\Core\Validation\ValidationFail; class EditorController { private TacticModel $model; @@ -22,7 +21,11 @@ class EditorController { * @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", [ + "id" => $tactic->getId(), + "name" => $tactic->getName(), + "content" => $tactic->getContent(), + ]); } /** diff --git a/src/Core/Data/TacticInfo.php b/src/Core/Data/TacticInfo.php index 04f592a..2565f93 100644 --- a/src/Core/Data/TacticInfo.php +++ b/src/Core/Data/TacticInfo.php @@ -8,17 +8,28 @@ class TacticInfo { private int $creationDate; private int $ownerId; + private string $content; + /** * @param int $id * @param string $name * @param int $creationDate * @param int $ownerId + * @param string $content */ - public function __construct(int $id, string $name, int $creationDate, int $ownerId) { + public function __construct(int $id, string $name, int $creationDate, int $ownerId, string $content) { $this->id = $id; $this->name = $name; $this->ownerId = $ownerId; $this->creationDate = $creationDate; + $this->content = $content; + } + + /** + * @return string + */ + public function getContent(): string { + return $this->content; } public function getId(): int { @@ -36,8 +47,10 @@ class TacticInfo { return $this->ownerId; } - public function getCreationTimestamp(): int { + /** + * @return int + */ + public function getCreationDate(): int { return $this->creationDate; } - } diff --git a/src/Core/Gateway/TacticInfoGateway.php b/src/Core/Gateway/TacticInfoGateway.php index f3d2432..3e0a0d0 100644 --- a/src/Core/Gateway/TacticInfoGateway.php +++ b/src/Core/Gateway/TacticInfoGateway.php @@ -33,7 +33,7 @@ class TacticInfoGateway { $row = $res[0]; - return new TacticInfo($id, $row["name"], strtotime($row["creation_date"]), $row["owner"]); + return new TacticInfo($id, $row["name"], strtotime($row["creation_date"]), $row["owner"], $row['content']); } @@ -57,9 +57,9 @@ class TacticInfoGateway { /** * @param string $name * @param int $owner - * @return TacticInfo + * @return int inserted tactic id */ - public function insert(string $name, int $owner): TacticInfo { + public function insert(string $name, int $owner): int { $this->con->exec( "INSERT INTO Tactic(name, owner) VALUES(:name, :owner)", [ @@ -67,11 +67,7 @@ class TacticInfoGateway { ":owner" => [$owner, PDO::PARAM_INT], ] ); - $row = $this->con->fetch( - "SELECT id, creation_date, owner FROM Tactic WHERE :id = id", - [':id' => [$this->con->lastInsertId(), PDO::PARAM_INT]] - )[0]; - return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"]), $row["owner"]); + return intval($this->con->lastInsertId()); } /** @@ -90,4 +86,14 @@ class TacticInfoGateway { ); } + public function updateContent(int $id, string $json): void { + $this->con->exec( + "UPDATE Tactic SET content = :content WHERE id = :id", + [ + ":content" => [$json, PDO::PARAM_STR], + ":id" => [$id, PDO::PARAM_INT], + ] + ); + } + } diff --git a/src/Core/Model/TacticModel.php b/src/Core/Model/TacticModel.php index 64c6ca3..0560568 100644 --- a/src/Core/Model/TacticModel.php +++ b/src/Core/Model/TacticModel.php @@ -2,9 +2,9 @@ namespace IQBall\Core\Model; +use IQBall\Core\Data\TacticInfo; use IQBall\Core\Gateway\TacticInfoGateway; use IQBall\Core\Validation\ValidationFail; -use IQBall\Core\Data\TacticInfo; class TacticModel { public const TACTIC_DEFAULT_NAME = "Nouvelle tactique"; @@ -26,7 +26,8 @@ class TacticModel { * @return TacticInfo */ public function makeNew(string $name, int $ownerId): TacticInfo { - return $this->tactics->insert($name, $ownerId); + $id = $this->tactics->insert($name, $ownerId); + return $this->tactics->get($id); } /** @@ -79,4 +80,8 @@ class TacticModel { return []; } + public function updateContent(int $id, string $json): void { + $this->tactics->updateContent($id, $json); + } + } From 2e4f2eb10c015c966e045d9d6ac66ea7e203a26a Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sun, 19 Nov 2023 22:18:24 +0100 Subject: [PATCH 22/75] add and configure tsc, phpstan, prettier and php-cs-fixer, format and fix reported code errors --- .gitignore | 2 + .php-cs-fixer.php | 16 ++ .prettierrc | 7 + Documentation/conception.puml | 12 -- Documentation/models.puml | 2 +- composer.json | 5 +- config.php | 3 +- format.sh | 9 ++ front/Constants.ts | 2 +- front/ViewRenderer.tsx | 16 +- front/components/Rack.tsx | 84 +++++----- front/components/TitleInput.tsx | 42 ++--- front/components/editor/BasketCourt.tsx | 34 ++-- front/components/editor/CourtPlayer.tsx | 66 ++++---- front/components/editor/PlayerPiece.tsx | 11 +- front/data/Player.ts | 12 +- front/data/Team.tsx | 4 +- front/style/basket_court.css | 7 +- front/style/colors.css | 6 +- front/style/editor.css | 7 +- front/style/player.css | 2 +- front/style/title_input.css | 1 - front/views/DisplayResults.tsx | 16 +- front/views/Editor.tsx | 172 +++++++++++---------- front/views/SampleForm.tsx | 11 +- package.json | 8 +- phpstan.neon | 15 ++ profiles/dev-config-profile.php | 6 +- profiles/prod-config-profile.php | 2 +- public/api/index.php | 4 +- public/index.php | 9 +- public/utils.php | 9 +- sql/database.php | 3 - src/Connexion.php | 22 +-- src/Controller/Api/APITacticController.php | 6 +- src/Controller/Control.php | 21 +-- src/Controller/EditorController.php | 3 +- src/Controller/ErrorController.php | 16 +- src/Controller/SampleFormController.php | 18 ++- src/Data/Account.php | 14 +- src/Data/AccountUser.php | 8 +- src/Data/Color.php | 4 +- src/Data/Member.php | 2 +- src/Data/MemberRole.php | 3 +- src/Data/TacticInfo.php | 7 +- src/Data/Team.php | 11 +- src/Data/User.php | 3 +- src/Gateway/FormResultGateway.php | 12 +- src/Gateway/TacticInfoGateway.php | 8 +- src/Http/HttpCodes.php | 2 +- src/Http/HttpRequest.php | 33 +++- src/Http/HttpResponse.php | 3 +- src/Http/JsonHttpResponse.php | 3 +- src/Http/ViewHttpResponse.php | 14 +- src/Model/TacticModel.php | 5 +- src/Validation/ComposedValidator.php | 3 +- src/Validation/FieldValidationFail.php | 8 +- src/Validation/FunctionValidator.php | 6 +- src/Validation/SimpleFunctionValidator.php | 9 +- src/Validation/Validation.php | 5 +- src/Validation/ValidationFail.php | 11 +- src/Validation/Validator.php | 7 +- src/Validation/Validators.php | 5 +- src/react-display.php | 6 +- verify.sh | 9 ++ 65 files changed, 507 insertions(+), 385 deletions(-) create mode 100644 .php-cs-fixer.php create mode 100644 .prettierrc delete mode 100644 Documentation/conception.puml create mode 100755 format.sh create mode 100644 phpstan.neon create mode 100755 verify.sh diff --git a/.gitignore b/.gitignore index 9124809..1c4404c 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ package-lock.json npm-debug.log* yarn-debug.log* yarn-error.log* + +.php-cs-fixer.cache \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..265de93 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,16 @@ +in(__DIR__); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PER-CS' => true, + '@PHP82Migration' => true, + 'array_syntax' => ['syntax' => 'short'], + 'braces_position' => [ + 'classes_opening_brace' => 'same_line', + 'functions_opening_brace' => 'same_line' + ] + ]) + ->setIndent(" ") + ->setFinder($finder); diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7db0434 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "bracketSameLine": true, + "trailingComma": "all", + "printWidth": 80, + "tabWidth": 4, + "semi": false +} \ No newline at end of file diff --git a/Documentation/conception.puml b/Documentation/conception.puml deleted file mode 100644 index 06ae256..0000000 --- a/Documentation/conception.puml +++ /dev/null @@ -1,12 +0,0 @@ -@startuml - -class Connexion - -class Modele - -class Account - -class AccountGateway - - -@enduml diff --git a/Documentation/models.puml b/Documentation/models.puml index 0ad5135..1f4877a 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -50,7 +50,6 @@ enum MemberRole { class Team { - name: String - picture: Url - - members: array + getName(): String + getPicture(): Url @@ -61,6 +60,7 @@ class Team { Team --> "- mainColor" Color Team --> "- secondaryColor" Color +Team --> "- members *" Member class Color { - value: int diff --git a/composer.json b/composer.json index e5b80e0..a3c0e4b 100644 --- a/composer.json +++ b/composer.json @@ -11,5 +11,8 @@ "ext-pdo_sqlite": "*", "twig/twig":"^2.0", "phpstan/phpstan": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.38" } -} \ No newline at end of file +} diff --git a/config.php b/config.php index 592ee38..6e510c8 100644 --- a/config.php +++ b/config.php @@ -5,7 +5,7 @@ // Please do not touch. require /*PROFILE_FILE*/ "profiles/dev-config-profile.php"; -CONST SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH; +const SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH; /** * Maps the given relative source uri (relative to the `/front` folder) to its actual location depending on imported profile. @@ -20,4 +20,3 @@ global $_data_source_name; $data_source_name = $_data_source_name; const DATABASE_USER = _DATABASE_USER; const DATABASE_PASSWORD = _DATABASE_PASSWORD; - diff --git a/format.sh b/format.sh new file mode 100755 index 0000000..a9ff1c2 --- /dev/null +++ b/format.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +## verify php and typescript types + +echo "formatting php typechecking" +vendor/bin/php-cs-fixer fix + +echo "formatting typescript typechecking" +npm run format diff --git a/front/Constants.ts b/front/Constants.ts index aaaaa43..76b37c2 100644 --- a/front/Constants.ts +++ b/front/Constants.ts @@ -1,4 +1,4 @@ /** * This constant defines the API endpoint. */ -export const API = import.meta.env.VITE_API_ENDPOINT; \ No newline at end of file +export const API = import.meta.env.VITE_API_ENDPOINT diff --git a/front/ViewRenderer.tsx b/front/ViewRenderer.tsx index ffaf886..57f2e34 100644 --- a/front/ViewRenderer.tsx +++ b/front/ViewRenderer.tsx @@ -1,5 +1,5 @@ -import ReactDOM from "react-dom/client"; -import React, {FunctionComponent} from "react"; +import ReactDOM from "react-dom/client" +import React, { FunctionComponent } from "react" /** * Dynamically renders a React component, with given arguments @@ -8,12 +8,12 @@ import React, {FunctionComponent} from "react"; */ export function renderView(Component: FunctionComponent, args: {}) { const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement - ); + document.getElementById("root") as HTMLElement, + ) root.render( - - - ); -} \ No newline at end of file + + , + ) +} diff --git a/front/components/Rack.tsx b/front/components/Rack.tsx index 09681e7..2a7511f 100644 --- a/front/components/Rack.tsx +++ b/front/components/Rack.tsx @@ -1,58 +1,72 @@ -import {ReactElement, useRef} from "react"; -import Draggable from "react-draggable"; +import { ReactElement, useRef } from "react" +import Draggable from "react-draggable" -export interface RackProps { - id: string, - objects: E[], - onChange: (objects: E[]) => void, - canDetach: (ref: HTMLDivElement) => boolean, - onElementDetached: (ref: HTMLDivElement, el: E) => void, - render: (e: E) => ReactElement, +export interface RackProps { + id: string + objects: E[] + onChange: (objects: E[]) => void + canDetach: (ref: HTMLDivElement) => boolean + onElementDetached: (ref: HTMLDivElement, el: E) => void + render: (e: E) => ReactElement } -interface RackItemProps { - item: E, - onTryDetach: (ref: HTMLDivElement, el: E) => void, - render: (e: E) => ReactElement, +interface RackItemProps { + item: E + onTryDetach: (ref: HTMLDivElement, el: E) => void + render: (e: E) => ReactElement } /** * A container of draggable objects * */ -export function Rack({id, objects, onChange, canDetach, onElementDetached, render}: RackProps) { +export function Rack({ + id, + objects, + onChange, + canDetach, + onElementDetached, + render, +}: RackProps) { return ( -
- {objects.map(element => ( - { - if (!canDetach(ref)) - return +
+ {objects.map((element) => ( + { + if (!canDetach(ref)) return - const index = objects.findIndex(o => o.key === element.key) - onChange(objects.toSpliced(index, 1)) + const index = objects.findIndex( + (o) => o.key === element.key, + ) + onChange(objects.toSpliced(index, 1)) - onElementDetached(ref, element) - }}/> + onElementDetached(ref, element) + }} + /> ))}
) } -function RackItem({item, onTryDetach, render}: RackItemProps) { - const divRef = useRef(null); +function RackItem({ + item, + onTryDetach, + render, +}: RackItemProps) { + const divRef = useRef(null) return ( onTryDetach(divRef.current!, item)}> -
- {render(item)} -
+
{render(item)}
) -} \ No newline at end of file +} diff --git a/front/components/TitleInput.tsx b/front/components/TitleInput.tsx index eb162d1..6e4acb0 100644 --- a/front/components/TitleInput.tsx +++ b/front/components/TitleInput.tsx @@ -1,28 +1,32 @@ -import React, {CSSProperties, useRef, useState} from "react"; -import "../style/title_input.css"; +import React, { CSSProperties, useRef, useState } from "react" +import "../style/title_input.css" export interface TitleInputOptions { - style: CSSProperties, - default_value: string, + style: CSSProperties + default_value: string on_validated: (a: string) => void } -export default function TitleInput({style, default_value, on_validated}: TitleInputOptions) { - const [value, setValue] = useState(default_value); - const ref = useRef(null); +export default function TitleInput({ + style, + default_value, + on_validated, +}: TitleInputOptions) { + const [value, setValue] = useState(default_value) + const ref = useRef(null) return ( - setValue(event.target.value)} - onBlur={_ => on_validated(value)} - onKeyDown={event => { - if (event.key == 'Enter') - ref.current?.blur(); - }} + setValue(event.target.value)} + onBlur={(_) => on_validated(value)} + onKeyDown={(event) => { + if (event.key == "Enter") ref.current?.blur() + }} /> ) -} \ No newline at end of file +} diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 7e839a8..9f4cb5d 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,25 +1,27 @@ -import CourtSvg from '../../assets/basketball_court.svg?react'; -import '../../style/basket_court.css'; -import {useRef} from "react"; -import CourtPlayer from "./CourtPlayer"; -import {Player} from "../../data/Player"; +import CourtSvg from "../../assets/basketball_court.svg?react" +import "../../style/basket_court.css" +import { useRef } from "react" +import CourtPlayer from "./CourtPlayer" +import { Player } from "../../data/Player" export interface BasketCourtProps { - players: Player[], - onPlayerRemove: (p: Player) => void, + players: Player[] + onPlayerRemove: (p: Player) => void } -export function BasketCourt({players, onPlayerRemove}: BasketCourtProps) { +export function BasketCourt({ players, onPlayerRemove }: BasketCourtProps) { return ( -
- - {players.map(player => { - return onPlayerRemove(player)} - /> +
+ + {players.map((player) => { + return ( + onPlayerRemove(player)} + /> + ) })}
) } - diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index b6b64de..9b08e7b 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,55 +1,49 @@ -import {useRef} from "react"; -import "../../style/player.css"; -import RemoveIcon from "../../assets/icon/remove.svg?react"; -import Draggable from "react-draggable"; -import {PlayerPiece} from "./PlayerPiece"; -import {Player} from "../../data/Player"; +import { useRef } from "react" +import "../../style/player.css" +import RemoveIcon from "../../assets/icon/remove.svg?react" +import Draggable from "react-draggable" +import { PlayerPiece } from "./PlayerPiece" +import { Player } from "../../data/Player" export interface PlayerProps { - player: Player, + player: Player onRemove: () => void } /** * A player that is placed on the court, which can be selected, and moved in the associated bounds * */ -export default function CourtPlayer({player, onRemove}: PlayerProps) { +export default function CourtPlayer({ player, onRemove }: PlayerProps) { + const ref = useRef(null) - const ref = useRef(null); - - const x = player.rightRatio; - const y = player.bottomRatio; + const x = player.rightRatio + const y = player.bottomRatio return ( - -
- -
{ - if (e.key == "Delete") - onRemove() - }}> + +
+
{ + if (e.key == "Delete") onRemove() + }}>
+ onClick={onRemove} + />
- +
-
- ) -} \ No newline at end of file +} diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx index 83e7dfc..08bf36d 100644 --- a/front/components/editor/PlayerPiece.tsx +++ b/front/components/editor/PlayerPiece.tsx @@ -1,12 +1,11 @@ -import React from "react"; -import '../../style/player.css' -import {Team} from "../../data/Team"; +import React from "react" +import "../../style/player.css" +import { Team } from "../../data/Team" - -export function PlayerPiece({team, text}: { team: Team, text: string }) { +export function PlayerPiece({ team, text }: { team: Team; text: string }) { return (

{text}

) -} \ No newline at end of file +} diff --git a/front/data/Player.ts b/front/data/Player.ts index af88c1c..f2667b9 100644 --- a/front/data/Player.ts +++ b/front/data/Player.ts @@ -1,21 +1,21 @@ -import {Team} from "./Team"; +import { Team } from "./Team" export interface Player { /** * unique identifier of the player. * This identifier must be unique to the associated court. */ - id: number, + id: number /** * the player's team * */ - team: Team, + team: Team /** * player's position * */ - role: string, + role: string /** * Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) @@ -25,5 +25,5 @@ export interface Player { /** * Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle) */ - rightRatio: number, -} \ No newline at end of file + rightRatio: number +} diff --git a/front/data/Team.tsx b/front/data/Team.tsx index ea4c384..5b35943 100644 --- a/front/data/Team.tsx +++ b/front/data/Team.tsx @@ -1,4 +1,4 @@ export enum Team { Allies = "allies", - Opponents = "opponents" -} \ No newline at end of file + Opponents = "opponents", +} diff --git a/front/style/basket_court.css b/front/style/basket_court.css index a5bc688..c001cc0 100644 --- a/front/style/basket_court.css +++ b/front/style/basket_court.css @@ -1,9 +1,6 @@ - - #court-container { display: flex; - background-color: var(--main-color); } @@ -13,8 +10,6 @@ -webkit-user-drag: none; } - - #court-svg * { stroke: var(--selected-team-secondarycolor); -} \ No newline at end of file +} diff --git a/front/style/colors.css b/front/style/colors.css index f3287cb..3c17a25 100644 --- a/front/style/colors.css +++ b/front/style/colors.css @@ -1,5 +1,3 @@ - - :root { --main-color: #ffffff; --second-color: #ccde54; @@ -9,5 +7,5 @@ --selected-team-primarycolor: #ffffff; --selected-team-secondarycolor: #000000; - --selection-color: #3f7fc4 -} \ No newline at end of file + --selection-color: #3f7fc4; +} diff --git a/front/style/editor.css b/front/style/editor.css index 3aad26c..b586a36 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -1,6 +1,5 @@ @import "colors.css"; - #main-div { display: flex; height: 100%; @@ -31,7 +30,8 @@ height: 100%; } -#allies-rack .player-piece , #opponent-rack .player-piece { +#allies-rack .player-piece, +#opponent-rack .player-piece { margin-left: 5px; } @@ -53,7 +53,6 @@ width: 60%; } - .react-draggable { z-index: 2; -} \ No newline at end of file +} diff --git a/front/style/player.css b/front/style/player.css index 264b479..7bea36e 100644 --- a/front/style/player.css +++ b/front/style/player.css @@ -76,4 +76,4 @@ on the court. .player:focus-within { z-index: 1000; -} \ No newline at end of file +} diff --git a/front/style/title_input.css b/front/style/title_input.css index 57af59b..1b6be10 100644 --- a/front/style/title_input.css +++ b/front/style/title_input.css @@ -14,4 +14,3 @@ border-bottom-color: blueviolet; } - diff --git a/front/views/DisplayResults.tsx b/front/views/DisplayResults.tsx index c4bbd1b..7e22df3 100644 --- a/front/views/DisplayResults.tsx +++ b/front/views/DisplayResults.tsx @@ -1,19 +1,13 @@ - interface DisplayResultsProps { - results: readonly { name: string, description: string}[] + results: readonly { name: string; description: string }[] } -export default function DisplayResults({results}: DisplayResultsProps) { - const list = results - .map(({name, description}) => +export default function DisplayResults({ results }: DisplayResultsProps) { + const list = results.map(({ name, description }) => (

username: {name}

description: {description}

- ) - return ( -
- {list} -
- ) + )) + return
{list}
} diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 423385f..d98062d 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -1,43 +1,42 @@ -import {CSSProperties, useRef, useState} from "react"; -import "../style/editor.css"; -import TitleInput from "../components/TitleInput"; -import {API} from "../Constants"; -import {BasketCourt} from "../components/editor/BasketCourt"; +import { CSSProperties, useRef, useState } from "react" +import "../style/editor.css" +import TitleInput from "../components/TitleInput" +import { API } from "../Constants" +import { BasketCourt } from "../components/editor/BasketCourt" -import {Rack} from "../components/Rack"; -import {PlayerPiece} from "../components/editor/PlayerPiece"; -import {Player} from "../data/Player"; -import {Team} from "../data/Team"; +import { Rack } from "../components/Rack" +import { PlayerPiece } from "../components/editor/PlayerPiece" +import { Player } from "../data/Player" +import { Team } from "../data/Team" const ERROR_STYLE: CSSProperties = { - borderColor: "red" + borderColor: "red", } /** * information about a player that is into a rack */ interface RackedPlayer { - team: Team, - key: string, + team: Team + key: string } -export default function Editor({id, name}: { id: number, name: string }) { - const [style, setStyle] = useState({}); - +export default function Editor({ id, name }: { id: number; name: string }) { + const [style, setStyle] = useState({}) const positions = ["1", "2", "3", "4", "5"] const [allies, setAllies] = useState( - positions.map(key => ({team: Team.Allies, key})) + positions.map((key) => ({ team: Team.Allies, key })), ) const [opponents, setOpponents] = useState( - positions.map(key => ({team: Team.Opponents, key})) + positions.map((key) => ({ team: Team.Opponents, key })), ) - const [players, setPlayers] = useState([]); - const courtDivContentRef = useRef(null); + const [players, setPlayers] = useState([]) + const courtDivContentRef = useRef(null) const canDetach = (ref: HTMLDivElement) => { - const refBounds = ref.getBoundingClientRect(); - const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); + const refBounds = ref.getBoundingClientRect() + const courtBounds = courtDivContentRef.current!.getBoundingClientRect() // check if refBounds overlaps courtBounds return !( @@ -45,27 +44,30 @@ export default function Editor({id, name}: { id: number, name: string }) { refBounds.right < courtBounds.left || refBounds.bottom < courtBounds.top || refBounds.left > courtBounds.right - ); + ) } const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => { - const refBounds = ref.getBoundingClientRect(); - const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); + const refBounds = ref.getBoundingClientRect() + const courtBounds = courtDivContentRef.current!.getBoundingClientRect() - const relativeXPixels = refBounds.x - courtBounds.x; - const relativeYPixels = refBounds.y - courtBounds.y; + const relativeXPixels = refBounds.x - courtBounds.x + const relativeYPixels = refBounds.y - courtBounds.y - const xRatio = relativeXPixels / courtBounds.width; - const yRatio = relativeYPixels / courtBounds.height; + const xRatio = relativeXPixels / courtBounds.width + const yRatio = relativeYPixels / courtBounds.height - setPlayers(players => { - return [...players, { - id: players.length, - team: element.team, - role: element.key, - rightRatio: xRatio, - bottomRatio: yRatio - }] + setPlayers((players) => { + return [ + ...players, + { + id: players.length, + team: element.team, + role: element.key, + rightRatio: xRatio, + bottomRatio: yRatio, + }, + ] }) } @@ -73,74 +75,88 @@ export default function Editor({id, name}: { id: number, name: string }) {
LEFT
- { - fetch(`${API}/tactic/${id}/edit/name`, { - method: "POST", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - name: new_name, + { + fetch(`${API}/tactic/${id}/edit/name`, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: new_name, + }), + }).then((response) => { + if (response.ok) { + setStyle({}) + } else { + setStyle(ERROR_STYLE) + } }) - }).then(response => { - if (response.ok) { - setStyle({}) - } else { - setStyle(ERROR_STYLE) - } - }) - }}/> + }} + />
RIGHT
- }/> - }/> + ( + + )} + /> + ( + + )} + />
{ - setPlayers(players => { + setPlayers((players) => { const idx = players.indexOf(player) return players.toSpliced(idx, 1) }) switch (player.team) { case Team.Opponents: - setOpponents(opponents => ( - [...opponents, { + setOpponents((opponents) => [ + ...opponents, + { team: player.team, pos: player.role, - key: player.role - }] - )) + key: player.role, + }, + ]) break case Team.Allies: - setAllies(allies => ( - [...allies, { + setAllies((allies) => [ + ...allies, + { team: player.team, pos: player.role, - key: player.role - }] - )) + key: player.role, + }, + ]) } - }}/> + }} + />
) } - diff --git a/front/views/SampleForm.tsx b/front/views/SampleForm.tsx index 604e362..00309e4 100644 --- a/front/views/SampleForm.tsx +++ b/front/views/SampleForm.tsx @@ -1,19 +1,14 @@ - - export default function SampleForm() { return (

Hello, this is a sample form made in react !

- + - - + +
) } - - - diff --git a/package.json b/package.json index 97f0039..a3ba0ad 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "scripts": { "start": "vite --host", "build": "vite build", - "test": "vite test" + "test": "vite test", + "format": "prettier --config .prettierrc 'front' --write", + "tsc": "node_modules/.bin/tsc" }, "eslintConfig": { "extends": [ @@ -30,6 +32,8 @@ }, "devDependencies": { "@vitejs/plugin-react": "^4.1.0", - "vite-plugin-svgr": "^4.1.0" + "vite-plugin-svgr": "^4.1.0", + "prettier": "^3.1.0", + "typescript": "^5.2.2" } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..7801d9b --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,15 @@ +parameters: + phpVersion: 70400 + level: 6 + paths: + - src + - public + - sql + ignoreErrors: + - + message: '#.*#' + path: sql/database.php + - + message: '#.*#' + path: src/react-display-file.php + diff --git a/profiles/dev-config-profile.php b/profiles/dev-config-profile.php index 316ff44..bd87f1d 100644 --- a/profiles/dev-config-profile.php +++ b/profiles/dev-config-profile.php @@ -10,11 +10,7 @@ $_data_source_name = "sqlite:${_SERVER['DOCUMENT_ROOT']}/../dev-database.sqlite" const _DATABASE_USER = null; const _DATABASE_PASSWORD = null; -function _asset(string $assetURI): string -{ +function _asset(string $assetURI): string { global $front_url; return $front_url . "/" . $assetURI; } - - - diff --git a/profiles/prod-config-profile.php b/profiles/prod-config-profile.php index e185dfc..e9bb12c 100644 --- a/profiles/prod-config-profile.php +++ b/profiles/prod-config-profile.php @@ -19,4 +19,4 @@ function _asset(string $assetURI): string { // If the asset uri does not figure in the available assets array, // fallback to the uri itself. return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI); -} \ No newline at end of file +} diff --git a/public/api/index.php b/public/api/index.php index b6327e1..3ed5caa 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -37,6 +37,6 @@ http_response_code($response->getCode()); if ($response instanceof JsonHttpResponse) { header('Content-type: application/json'); echo $response->getJson(); -} else if ($response instanceof ViewHttpResponse) { +} elseif ($response instanceof ViewHttpResponse) { throw new Exception("API returned a view http response."); -} \ No newline at end of file +} diff --git a/public/index.php b/public/index.php index ba9d7c0..e81d098 100644 --- a/public/index.php +++ b/public/index.php @@ -17,7 +17,6 @@ use Twig\Loader\FilesystemLoader; use App\Validation\ValidationFail; use App\Controller\ErrorController; - $loader = new FilesystemLoader('../src/Views/'); $twig = new \Twig\Environment($loader); @@ -28,7 +27,7 @@ $con = new Connexion(get_database()); $router = new AltoRouter(); $router->setBasePath($basePath); -$sampleFormController = new SampleFormController(new FormResultGateway($con), $twig); +$sampleFormController = new SampleFormController(new FormResultGateway($con)); $editorController = new EditorController(new TacticModel(new TacticInfoGateway($con))); @@ -65,12 +64,12 @@ if ($response instanceof ViewHttpResponse) { } catch (\Twig\Error\RuntimeError|\Twig\Error\SyntaxError $e) { http_response_code(500); echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); - throw e; + throw $e; } break; } -} else if ($response instanceof JsonHttpResponse) { +} elseif ($response instanceof JsonHttpResponse) { header('Content-type: application/json'); echo $response->getJson(); -} \ No newline at end of file +} diff --git a/public/utils.php b/public/utils.php index a3566fe..7151386 100644 --- a/public/utils.php +++ b/public/utils.php @@ -3,18 +3,19 @@ /** * relative path of the public directory from the server's document root. */ -function get_public_path() { +function get_public_path(): string { // find the server path of the index.php file $basePath = substr(__DIR__, strlen($_SERVER['DOCUMENT_ROOT'])); $basePathLen = strlen($basePath); - if ($basePathLen == 0) + if ($basePathLen == 0) { return ""; - + } + $c = $basePath[$basePathLen - 1]; if ($c == "/" || $c == "\\") { $basePath = substr($basePath, 0, $basePathLen - 1); } return $basePath; -} \ No newline at end of file +} diff --git a/sql/database.php b/sql/database.php index d49ddfd..8f5aa9d 100644 --- a/sql/database.php +++ b/sql/database.php @@ -25,6 +25,3 @@ function get_database(): PDO { return $pdo; } - - - diff --git a/src/Connexion.php b/src/Connexion.php index 788c0fb..987d35b 100644 --- a/src/Connexion.php +++ b/src/Connexion.php @@ -1,28 +1,27 @@ pdo = $pdo; } - public function lastInsertId() { + public function lastInsertId(): string { return $this->pdo->lastInsertId(); } /** * execute a request * @param string $query - * @param array $args + * @param array> $args * @return void */ public function exec(string $query, array $args) { @@ -33,8 +32,8 @@ class Connexion { /** * Execute a request, and return the returned rows * @param string $query the SQL request - * @param array $args an array containing the arguments label, value and type: ex: `[":label" => [$value, PDO::PARAM_TYPE]` - * @return array the returned rows of the request + * @param array> $args an array containing the arguments label, value and type: ex: `[":label" => [$value, PDO::PARAM_TYPE]` + * @return array[] the returned rows of the request */ public function fetch(string $query, array $args): array { $stmnt = $this->prepare($query, $args); @@ -42,6 +41,11 @@ class Connexion { return $stmnt->fetchAll(PDO::FETCH_ASSOC); } + /** + * @param string $query + * @param array> $args + * @return \PDOStatement + */ private function prepare(string $query, array $args): \PDOStatement { $stmnt = $this->pdo->prepare($query); foreach ($args as $name => $value) { @@ -50,4 +54,4 @@ class Connexion { return $stmnt; } -} \ No newline at end of file +} diff --git a/src/Controller/Api/APITacticController.php b/src/Controller/Api/APITacticController.php index a39b2ce..ec0edc8 100644 --- a/src/Controller/Api/APITacticController.php +++ b/src/Controller/Api/APITacticController.php @@ -25,7 +25,7 @@ class APITacticController { public function updateName(int $tactic_id): HttpResponse { return Control::runChecked([ - "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()] + "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], ], function (HttpRequest $request) use ($tactic_id) { $this->model->updateName($tactic_id, $request["name"]); return HttpResponse::fromCode(HttpCodes::OK); @@ -34,7 +34,7 @@ class APITacticController { public function newTactic(): HttpResponse { return Control::runChecked([ - "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()] + "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], ], function (HttpRequest $request) { $tactic = $this->model->makeNew($request["name"]); $id = $tactic->getId(); @@ -52,4 +52,4 @@ class APITacticController { return new JsonHttpResponse($tactic_info); } -} \ No newline at end of file +} diff --git a/src/Controller/Control.php b/src/Controller/Control.php index 8c00d2e..e50eed0 100644 --- a/src/Controller/Control.php +++ b/src/Controller/Control.php @@ -8,14 +8,16 @@ use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; use App\Validation\ValidationFail; +use App\Validation\Validator; class Control { - /** * Runs given callback, if the request's json validates the given schema. - * @param array $schema an array of `fieldName => Validators` which represents the request object schema + * @param array $schema an array of `fieldName => Validators` which represents the request object schema * @param callable $run the callback to run if the request is valid according to the given schema. * THe callback must accept an HttpRequest, and return an HttpResponse object. + * @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead + * of the ViewHttpResponse for an error view. * @return HttpResponse */ public static function runChecked(array $schema, callable $run, bool $errorInJson): HttpResponse { @@ -23,7 +25,7 @@ class Control { $payload_obj = json_decode($request_body); if (!$payload_obj instanceof \stdClass) { $fail = new ValidationFail("bad-payload", "request body is not a valid json object"); - if($errorInJson) { + if ($errorInJson) { return new JsonHttpResponse([$fail, HttpCodes::BAD_REQUEST]); } return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST); @@ -34,10 +36,12 @@ class Control { /** * Runs given callback, if the given request data array validates the given schema. - * @param array $data the request's data array. - * @param array $schema an array of `fieldName => Validators` which represents the request object schema + * @param array $data the request's data array. + * @param array $schema an array of `fieldName => Validators` which represents the request object schema * @param callable $run the callback to run if the request is valid according to the given schema. * THe callback must accept an HttpRequest, and return an HttpResponse object. + * @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead + * of the ViewHttpResponse for an error view. * @return HttpResponse */ public static function runCheckedFrom(array $data, array $schema, callable $run, bool $errorInJson): HttpResponse { @@ -45,7 +49,7 @@ class Control { $request = HttpRequest::from($data, $fails, $schema); if (!empty($fails)) { - if($errorInJson) { + if ($errorInJson) { return new JsonHttpResponse($fails, HttpCodes::BAD_REQUEST); } return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST); @@ -53,9 +57,6 @@ class Control { return call_user_func_array($run, [$request]); } - - - -} \ No newline at end of file +} diff --git a/src/Controller/EditorController.php b/src/Controller/EditorController.php index bf5dccc..ed270d1 100644 --- a/src/Controller/EditorController.php +++ b/src/Controller/EditorController.php @@ -11,7 +11,6 @@ use App\Http\ViewHttpResponse; use App\Model\TacticModel; class EditorController { - private TacticModel $model; /** @@ -45,4 +44,4 @@ class EditorController { return $this->openEditor($tactic); } -} \ No newline at end of file +} diff --git a/src/Controller/ErrorController.php b/src/Controller/ErrorController.php index e91d05f..7fc5239 100644 --- a/src/Controller/ErrorController.php +++ b/src/Controller/ErrorController.php @@ -3,17 +3,23 @@ namespace App\Controller; require_once __DIR__ . "/../react-display.php"; -use \Twig\Environment; + +use App\Validation\ValidationFail; +use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; -class ErrorController -{ - public static function displayFailures(array $failures, Environment $twig) { +class ErrorController { + /** + * @param ValidationFail[] $failures + * @param Environment $twig + * @return void + */ + public static function displayFailures(array $failures, Environment $twig): void { try { $twig->display("error.html.twig", ['failures' => $failures]); - } catch (LoaderError | RuntimeError | SyntaxError $e) { + } catch (LoaderError|RuntimeError|SyntaxError $e) { echo "Twig error: $e"; } } diff --git a/src/Controller/SampleFormController.php b/src/Controller/SampleFormController.php index 4241ad4..41b5f96 100644 --- a/src/Controller/SampleFormController.php +++ b/src/Controller/SampleFormController.php @@ -11,7 +11,6 @@ use App\Http\ViewHttpResponse; use App\Validation\Validators; class SampleFormController { - private FormResultGateway $gateway; /** @@ -30,10 +29,15 @@ class SampleFormController { return ViewHttpResponse::twig('sample_form.html.twig', []); } + /** + * @param array $form + * @param callable $response + * @return HttpResponse + */ private function submitForm(array $form, callable $response): HttpResponse { return Control::runCheckedFrom($form, [ "name" => [Validators::lenBetween(0, 32), Validators::name("Le nom ne peut contenir que des lettres, des chiffres et des accents")], - "description" => [Validators::lenBetween(0, 512)] + "description" => [Validators::lenBetween(0, 512)], ], function (HttpRequest $req) use ($response) { $description = htmlspecialchars($req["description"]); $this->gateway->insert($req["name"], $description); @@ -42,11 +46,19 @@ class SampleFormController { }, false); } + /** + * @param array $form + * @return HttpResponse + */ public function submitFormTwig(array $form): HttpResponse { return $this->submitForm($form, fn(array $results) => ViewHttpResponse::twig('display_results.html.twig', $results)); } + /** + * @param array $form + * @return HttpResponse + */ public function submitFormReact(array $form): HttpResponse { return $this->submitForm($form, fn(array $results) => ViewHttpResponse::react('views/DisplayResults.tsx', $results)); } -} \ No newline at end of file +} diff --git a/src/Data/Account.php b/src/Data/Account.php index 155f2ae..51d7aad 100755 --- a/src/Data/Account.php +++ b/src/Data/Account.php @@ -4,7 +4,7 @@ namespace App\Data; use http\Exception\InvalidArgumentException; -const PHONE_NUMBER_REGEXP = "\\+[0-9]+"; +const PHONE_NUMBER_REGEXP = "/^\\+[0-9]+$/"; /** * Base class of a user account. @@ -29,7 +29,7 @@ class Account { private AccountUser $user; /** - * @var array account's teams + * @var array account's teams */ private array $teams; @@ -38,10 +38,13 @@ class Account { */ private int $id; + /** * @param string $email * @param string $phoneNumber * @param AccountUser $user + * @param array $teams + * @param int $id */ public function __construct(string $email, string $phoneNumber, AccountUser $user, array $teams, int $id) { $this->email = $email; @@ -79,7 +82,7 @@ class Account { * @param string $phoneNumber */ public function setPhoneNumber(string $phoneNumber): void { - if (!filter_var($phoneNumber, FILTER_VALIDATE_REGEXP, PHONE_NUMBER_REGEXP)) { + if (!preg_match(PHONE_NUMBER_REGEXP, $phoneNumber)) { throw new InvalidArgumentException("Invalid phone number"); } $this->phoneNumber = $phoneNumber; @@ -89,6 +92,9 @@ class Account { return $this->id; } + /** + * @return Team[] + */ public function getTeams(): array { return $this->teams; } @@ -96,4 +102,4 @@ class Account { public function getUser(): AccountUser { return $this->user; } -} \ No newline at end of file +} diff --git a/src/Data/AccountUser.php b/src/Data/AccountUser.php index 7808062..5d3b497 100755 --- a/src/Data/AccountUser.php +++ b/src/Data/AccountUser.php @@ -36,17 +36,17 @@ class AccountUser implements User { return $this->age; } - public function setName(string $name) { + public function setName(string $name): void { $this->name = $name; } - public function setProfilePicture(Url $profilePicture) { + public function setProfilePicture(Url $profilePicture): void { $this->profilePicture = $profilePicture; } - public function setAge(int $age) { + public function setAge(int $age): void { $this->age = $age; } -} \ No newline at end of file +} diff --git a/src/Data/Color.php b/src/Data/Color.php index f841731..0b1fbb3 100755 --- a/src/Data/Color.php +++ b/src/Data/Color.php @@ -14,7 +14,7 @@ class Color { * @param int $value 6 bytes unsigned int that represents an RGB color * @throws \InvalidArgumentException if the value is negative or greater than 0xFFFFFF */ - public function __constructor(int $value) { + public function __construct(int $value) { if ($value < 0 || $value > 0xFFFFFF) { throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF"); } @@ -27,4 +27,4 @@ class Color { public function getValue(): int { return $this->value; } -} \ No newline at end of file +} diff --git a/src/Data/Member.php b/src/Data/Member.php index 91b09c4..6500733 100755 --- a/src/Data/Member.php +++ b/src/Data/Member.php @@ -39,4 +39,4 @@ class Member { return $this->role; } -} \ No newline at end of file +} diff --git a/src/Data/MemberRole.php b/src/Data/MemberRole.php index 05d746d..a1d4d31 100755 --- a/src/Data/MemberRole.php +++ b/src/Data/MemberRole.php @@ -2,7 +2,6 @@ namespace App\Data; - use http\Exception\InvalidArgumentException; /** @@ -37,4 +36,4 @@ final class MemberRole { return ($this->value == self::ROLE_COACH); } -} \ No newline at end of file +} diff --git a/src/Data/TacticInfo.php b/src/Data/TacticInfo.php index 901280d..eef7bb3 100644 --- a/src/Data/TacticInfo.php +++ b/src/Data/TacticInfo.php @@ -30,7 +30,10 @@ class TacticInfo implements \JsonSerializable { return $this->creation_date; } - public function jsonSerialize() { + /** + * @return array + */ + public function jsonSerialize(): array { return get_object_vars($this); } -} \ No newline at end of file +} diff --git a/src/Data/Team.php b/src/Data/Team.php index 48643d9..a224bd4 100755 --- a/src/Data/Team.php +++ b/src/Data/Team.php @@ -11,7 +11,7 @@ class Team { private Color $secondColor; /** - * @var array maps users with their role + * @var array maps users with their role */ private array $members; @@ -20,7 +20,7 @@ class Team { * @param Url $picture * @param Color $mainColor * @param Color $secondColor - * @param array $members + * @param array $members */ public function __construct(string $name, Url $picture, Color $mainColor, Color $secondColor, array $members) { $this->name = $name; @@ -58,8 +58,11 @@ class Team { return $this->secondColor; } + /** + * @return array + */ public function listMembers(): array { - return array_map(fn ($id, $role) => new Member($id, $role), $this->members); + return $this->members; } -} \ No newline at end of file +} diff --git a/src/Data/User.php b/src/Data/User.php index 15c9995..6cb55c2 100755 --- a/src/Data/User.php +++ b/src/Data/User.php @@ -4,7 +4,6 @@ namespace App\Data; use http\Url; - /** * Public information about a user */ @@ -24,4 +23,4 @@ interface User { * @return int The user's age */ public function getAge(): int; -} \ No newline at end of file +} diff --git a/src/Gateway/FormResultGateway.php b/src/Gateway/FormResultGateway.php index fe0c601..36178ad 100644 --- a/src/Gateway/FormResultGateway.php +++ b/src/Gateway/FormResultGateway.php @@ -9,7 +9,6 @@ use App\Connexion; * A sample gateway, that stores the sample form's result. */ class FormResultGateway { - private Connexion $con; public function __construct(Connexion $con) { @@ -17,17 +16,20 @@ class FormResultGateway { } - function insert(string $username, string $description) { + public function insert(string $username, string $description): void { $this->con->exec( "INSERT INTO FormEntries VALUES (:name, :description)", [ ":name" => [$username, PDO::PARAM_STR], - "description" => [$description, PDO::PARAM_STR] + "description" => [$description, PDO::PARAM_STR], ] ); } - function listResults(): array { + /** + * @return array + */ + public function listResults(): array { return $this->con->fetch("SELECT * FROM FormEntries", []); } -} \ No newline at end of file +} diff --git a/src/Gateway/TacticInfoGateway.php b/src/Gateway/TacticInfoGateway.php index 20d2957..3441c9a 100644 --- a/src/Gateway/TacticInfoGateway.php +++ b/src/Gateway/TacticInfoGateway.php @@ -4,7 +4,7 @@ namespace App\Gateway; use App\Connexion; use App\Data\TacticInfo; -use \PDO; +use PDO; class TacticInfoGateway { private Connexion $con; @@ -43,14 +43,14 @@ class TacticInfoGateway { return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"])); } - public function updateName(int $id, string $name) { + public function updateName(int $id, string $name): void { $this->con->exec( "UPDATE TacticInfo SET name = :name WHERE id = :id", [ ":name" => [$name, PDO::PARAM_STR], - ":id" => [$id, PDO::PARAM_INT] + ":id" => [$id, PDO::PARAM_INT], ] ); } -} \ No newline at end of file +} diff --git a/src/Http/HttpCodes.php b/src/Http/HttpCodes.php index b41af8a..f9d550c 100644 --- a/src/Http/HttpCodes.php +++ b/src/Http/HttpCodes.php @@ -10,4 +10,4 @@ class HttpCodes { public const BAD_REQUEST = 400; public const NOT_FOUND = 404; -} \ No newline at end of file +} diff --git a/src/Http/HttpRequest.php b/src/Http/HttpRequest.php index f841752..6414d75 100644 --- a/src/Http/HttpRequest.php +++ b/src/Http/HttpRequest.php @@ -4,12 +4,23 @@ namespace App\Http; use App\Validation\FieldValidationFail; use App\Validation\Validation; +use App\Validation\ValidationFail; +use App\Validation\Validator; use ArrayAccess; use Exception; +/** + * @implements ArrayAccess + * */ class HttpRequest implements ArrayAccess { + /** + * @var array + */ private array $data; + /** + * @param array $data + */ private function __construct(array $data) { $this->data = $data; } @@ -17,9 +28,9 @@ class HttpRequest implements ArrayAccess { /** * Creates a new HttpRequest instance, and ensures that the given request data validates the given schema. * This is a simple function that only supports flat schemas (non-composed, the data must only be a k/v array pair.) - * @param array $request the request's data - * @param array $fails a reference to a failure array, that will contain the reported validation failures. - * @param array $schema the schema to satisfy. a schema is a simple array with a string key (which is the top-level field name), and a set of validators + * @param array $request the request's data + * @param array $fails a reference to a failure array, that will contain the reported validation failures. + * @param array> $schema the schema to satisfy. a schema is a simple array with a string key (which is the top-level field name), and a set of validators * @return HttpRequest|null the built HttpRequest instance, or null if a field is missing, or if any of the schema validator failed */ public static function from(array $request, array &$fails, array $schema): ?HttpRequest { @@ -43,15 +54,29 @@ class HttpRequest implements ArrayAccess { return isset($this->data[$offset]); } + /** + * @param $offset + * @return mixed + */ public function offsetGet($offset) { return $this->data[$offset]; } + /** + * @param $offset + * @param $value + * @return mixed + * @throws Exception + */ public function offsetSet($offset, $value) { throw new Exception("requests are immutable objects."); } + /** + * @param $offset + * @throws Exception + */ public function offsetUnset($offset) { throw new Exception("requests are immutable objects."); } -} \ No newline at end of file +} diff --git a/src/Http/HttpResponse.php b/src/Http/HttpResponse.php index 9f081a5..5d8c3bf 100644 --- a/src/Http/HttpResponse.php +++ b/src/Http/HttpResponse.php @@ -3,7 +3,6 @@ namespace App\Http; class HttpResponse { - private int $code; /** @@ -21,4 +20,4 @@ class HttpResponse { return new HttpResponse($code); } -} \ No newline at end of file +} diff --git a/src/Http/JsonHttpResponse.php b/src/Http/JsonHttpResponse.php index 9d7423f..bbd3d80 100644 --- a/src/Http/JsonHttpResponse.php +++ b/src/Http/JsonHttpResponse.php @@ -3,7 +3,6 @@ namespace App\Http; class JsonHttpResponse extends HttpResponse { - /** * @var mixed Any JSON serializable value */ @@ -26,4 +25,4 @@ class JsonHttpResponse extends HttpResponse { return $result; } -} \ No newline at end of file +} diff --git a/src/Http/ViewHttpResponse.php b/src/Http/ViewHttpResponse.php index 0e92054..2e517d7 100644 --- a/src/Http/ViewHttpResponse.php +++ b/src/Http/ViewHttpResponse.php @@ -3,7 +3,6 @@ namespace App\Http; class ViewHttpResponse extends HttpResponse { - public const TWIG_VIEW = 0; public const REACT_VIEW = 1; @@ -12,7 +11,7 @@ class ViewHttpResponse extends HttpResponse { */ private string $file; /** - * @var array View arguments + * @var array View arguments */ private array $arguments; /** @@ -24,7 +23,7 @@ class ViewHttpResponse extends HttpResponse { * @param int $code * @param int $kind * @param string $file - * @param array $arguments + * @param array $arguments */ private function __construct(int $kind, string $file, array $arguments, int $code = HttpCodes::OK) { parent::__construct($code); @@ -41,6 +40,9 @@ class ViewHttpResponse extends HttpResponse { return $this->file; } + /** + * @return array + */ public function getArguments(): array { return $this->arguments; } @@ -48,7 +50,7 @@ class ViewHttpResponse extends HttpResponse { /** * Create a twig view response * @param string $file - * @param array $arguments + * @param array $arguments * @param int $code * @return ViewHttpResponse */ @@ -59,7 +61,7 @@ class ViewHttpResponse extends HttpResponse { /** * Create a react view response * @param string $file - * @param array $arguments + * @param array $arguments * @param int $code * @return ViewHttpResponse */ @@ -67,4 +69,4 @@ class ViewHttpResponse extends HttpResponse { return new ViewHttpResponse(self::REACT_VIEW, $file, $arguments, $code); } -} \ No newline at end of file +} diff --git a/src/Model/TacticModel.php b/src/Model/TacticModel.php index c0b1ffe..cabcdec 100644 --- a/src/Model/TacticModel.php +++ b/src/Model/TacticModel.php @@ -6,7 +6,6 @@ use App\Data\TacticInfo; use App\Gateway\TacticInfoGateway; class TacticModel { - public const TACTIC_DEFAULT_NAME = "Nouvelle tactique"; @@ -40,7 +39,7 @@ class TacticModel { * Update the name of a tactic * @param int $id the tactic identifier * @param string $name the new name to set - * @return true if the update was done successfully + * @return bool true if the update was done successfully */ public function updateName(int $id, string $name): bool { if ($this->tactics->get($id) == null) { @@ -51,4 +50,4 @@ class TacticModel { return true; } -} \ No newline at end of file +} diff --git a/src/Validation/ComposedValidator.php b/src/Validation/ComposedValidator.php index 418b1ed..cc6e9e5 100644 --- a/src/Validation/ComposedValidator.php +++ b/src/Validation/ComposedValidator.php @@ -3,7 +3,6 @@ namespace App\Validation; class ComposedValidator extends Validator { - private Validator $first; private Validator $then; @@ -21,4 +20,4 @@ class ComposedValidator extends Validator { $thenFailures = $this->then->validate($name, $val); return array_merge($firstFailures, $thenFailures); } -} \ No newline at end of file +} diff --git a/src/Validation/FieldValidationFail.php b/src/Validation/FieldValidationFail.php index 5b535f7..af2ce3a 100644 --- a/src/Validation/FieldValidationFail.php +++ b/src/Validation/FieldValidationFail.php @@ -2,7 +2,6 @@ namespace App\Validation; - /** * An error that concerns a field, with a bound message name */ @@ -34,7 +33,10 @@ class FieldValidationFail extends ValidationFail { return new FieldValidationFail($fieldName, "field is missing"); } - public function jsonSerialize() { + /** + * @return array + */ + public function jsonSerialize(): array { return ["field" => $this->fieldName, "message" => $this->getMessage()]; } -} \ No newline at end of file +} diff --git a/src/Validation/FunctionValidator.php b/src/Validation/FunctionValidator.php index 6874d63..f052ea6 100644 --- a/src/Validation/FunctionValidator.php +++ b/src/Validation/FunctionValidator.php @@ -3,7 +3,9 @@ namespace App\Validation; class FunctionValidator extends Validator { - + /** + * @var callable + */ private $validate_fn; /** @@ -16,4 +18,4 @@ class FunctionValidator extends Validator { public function validate(string $name, $val): array { return call_user_func_array($this->validate_fn, [$name, $val]); } -} \ No newline at end of file +} diff --git a/src/Validation/SimpleFunctionValidator.php b/src/Validation/SimpleFunctionValidator.php index 079452d..7514bae 100644 --- a/src/Validation/SimpleFunctionValidator.php +++ b/src/Validation/SimpleFunctionValidator.php @@ -6,8 +6,13 @@ namespace App\Validation; * A simple validator that takes a predicate and an error factory */ class SimpleFunctionValidator extends Validator { - + /** + * @var callable + */ private $predicate; + /** + * @var callable + */ private $errorFactory; /** @@ -25,4 +30,4 @@ class SimpleFunctionValidator extends Validator { } return []; } -} \ No newline at end of file +} diff --git a/src/Validation/Validation.php b/src/Validation/Validation.php index 4372380..19144b7 100644 --- a/src/Validation/Validation.php +++ b/src/Validation/Validation.php @@ -6,12 +6,11 @@ namespace App\Validation; * Utility class for validation */ class Validation { - /** * Validate a value from validators, appending failures in the given errors array. * @param mixed $val the value to validate * @param string $valName the name of the value - * @param array $failures array to push when a validator fails + * @param array $failures array to push when a validator fails * @param Validator ...$validators given validators * @return bool true if any of the given validators did fail */ @@ -27,4 +26,4 @@ class Validation { return $had_errors; } -} \ No newline at end of file +} diff --git a/src/Validation/ValidationFail.php b/src/Validation/ValidationFail.php index fa5139c..4f1ec22 100644 --- a/src/Validation/ValidationFail.php +++ b/src/Validation/ValidationFail.php @@ -2,7 +2,9 @@ namespace App\Validation; -class ValidationFail implements \JsonSerializable { +use JsonSerializable; + +class ValidationFail implements JsonSerializable { private string $kind; private string $message; @@ -24,7 +26,10 @@ class ValidationFail implements \JsonSerializable { return $this->kind; } - public function jsonSerialize() { + /** + * @return array + */ + public function jsonSerialize(): array { return ["error" => $this->kind, "message" => $this->message]; } @@ -32,4 +37,4 @@ class ValidationFail implements \JsonSerializable { return new ValidationFail("not found", $message); } -} \ No newline at end of file +} diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index 6cdafb9..4ad6131 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -3,14 +3,13 @@ namespace App\Validation; abstract class Validator { - /** * validates a variable string * @param string $name the name of the tested value * @param mixed $val the value to validate - * @return array the errors the validator has reported + * @return array the errors the validator has reported */ - public abstract function validate(string $name, $val): array; + abstract public function validate(string $name, $val): array; /** * Creates a validator composed of this validator, and given validator @@ -21,4 +20,4 @@ abstract class Validator { return new ComposedValidator($this, $other); } -} \ No newline at end of file +} diff --git a/src/Validation/Validators.php b/src/Validation/Validators.php index ea9da46..6f72cf2 100644 --- a/src/Validation/Validators.php +++ b/src/Validation/Validators.php @@ -6,7 +6,6 @@ namespace App\Validation; * A collection of standard validators */ class Validators { - /** * @return Validator a validator that validates a given regex */ @@ -20,7 +19,7 @@ class Validators { /** * @return Validator a validator that validates strings that only contains numbers, letters, accents letters, `-` and `_`. */ - public static function name($msg = null): Validator { + public static function name(string $msg = null): Validator { return self::regex("/^[0-9a-zA-Zà-üÀ-Ü_-]*$/", $msg); } @@ -51,4 +50,4 @@ class Validators { } ); } -} \ No newline at end of file +} diff --git a/src/react-display.php b/src/react-display.php index b965a3a..5baf41b 100644 --- a/src/react-display.php +++ b/src/react-display.php @@ -3,11 +3,11 @@ /** * sends a react view to the user client. * @param string $url url of the react file to render - * @param array $arguments arguments to pass to the rendered react component - * The arguments must be a json-encodable key/value dictionary. + * @param array $arguments arguments to pass to the rendered react component + * The arguments must be a json-encodable key/value dictionary. * @return void */ function send_react_front(string $url, array $arguments) { // the $url and $argument values are used into the included file require_once "react-display-file.php"; -} \ No newline at end of file +} diff --git a/verify.sh b/verify.sh new file mode 100755 index 0000000..314b8bc --- /dev/null +++ b/verify.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +## verify php and typescript types + +echo "running php typechecking" +vendor/bin/phpstan analyze && echo "php types are respected" + +echo "running typescript typechecking" +npm run tsc && echo "typescript types are respected" \ No newline at end of file From 185a4d19bbf233fc70dd5ee7ee99bf48d7a353df Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sun, 19 Nov 2023 22:20:22 +0100 Subject: [PATCH 23/75] add typecheckers in ci --- ci/.drone.yml | 22 +++++++++++++++++++--- ci/build_react.msh | 3 ++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/ci/.drone.yml b/ci/.drone.yml index d6c474c..2431032 100644 --- a/ci/.drone.yml +++ b/ci/.drone.yml @@ -1,6 +1,6 @@ kind: pipeline type: docker -name: "Deploy on maxou.dev" +name: "CI and Deploy on maxou.dev" volumes: - name: server @@ -11,11 +11,26 @@ trigger: - push steps: + + - image: node:latest + name: "front CI" + commands: + - npm install + - npm run tsc + + - image: composer:latest + name: "php CI" + commands: + - composer install && composer update + - vendor/bin/phpstan analyze + - image: node:latest name: "build node" volumes: &outputs - name: server path: /outputs + depends_on: + - "front CI" commands: - curl -L moshell.dev/setup.sh > /tmp/moshell_setup.sh - chmod +x /tmp/moshell_setup.sh @@ -24,14 +39,15 @@ steps: - - /root/.local/bin/moshell ci/build_react.msh - - image: composer:latest + - image: ubuntu:latest name: "prepare php" volumes: *outputs + depends_on: + - "php CI" commands: - mkdir -p /outputs/public # this sed command will replace the included `profile/dev-config-profile.php` to `profile/prod-config-file.php` in the config.php file. - sed -iE 's/\\/\\*PROFILE_FILE\\*\\/\\s*".*"/"profiles\\/prod-config-profile.php"/' config.php - - composer install && composer update - rm profiles/dev-config-profile.php - mv src config.php sql profiles vendor /outputs/ diff --git a/ci/build_react.msh b/ci/build_react.msh index c893498..64a0cb6 100755 --- a/ci/build_react.msh +++ b/ci/build_react.msh @@ -3,13 +3,14 @@ mkdir -p /outputs/public apt update && apt install jq -y -npm install val drone_branch = std::env("DRONE_BRANCH").unwrap() val base = "/IQBall/$drone_branch/public" npm run build -- --base=$base --mode PROD +npm run build -- --base=/IQBall/public --mode PROD + // Read generated mappings from build val result = $(jq -r 'to_entries|map(.key + " " +.value.file)|.[]' dist/manifest.json) val mappings = $result.split('\n') From bb53114d78de19b11dcb0645bf11afeaecfb7dac Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Tue, 21 Nov 2023 10:23:01 +0100 Subject: [PATCH 24/75] apply suggestions --- .php-cs-fixer.php | 2 +- ci/.drone.yml | 2 +- package.json | 2 +- phpstan.neon | 16 +++++++--------- src/Controller/Control.php | 4 ++-- src/Controller/SampleFormController.php | 2 +- src/Data/Account.php | 4 ++-- src/Data/Team.php | 6 +++--- src/Http/HttpRequest.php | 3 +-- src/Validation/FunctionValidator.php | 4 ++-- src/Validation/SimpleFunctionValidator.php | 8 ++++---- src/Validation/Validation.php | 2 +- src/Validation/Validator.php | 2 +- src/Validation/Validators.php | 4 ++-- 14 files changed, 29 insertions(+), 32 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 265de93..77ef0e7 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -5,7 +5,7 @@ $finder = (new PhpCsFixer\Finder())->in(__DIR__); return (new PhpCsFixer\Config()) ->setRules([ '@PER-CS' => true, - '@PHP82Migration' => true, + '@PHP74Migration' => true, 'array_syntax' => ['syntax' => 'short'], 'braces_position' => [ 'classes_opening_brace' => 'same_line', diff --git a/ci/.drone.yml b/ci/.drone.yml index 2431032..8b7058d 100644 --- a/ci/.drone.yml +++ b/ci/.drone.yml @@ -21,7 +21,7 @@ steps: - image: composer:latest name: "php CI" commands: - - composer install && composer update + - composer install - vendor/bin/phpstan analyze - image: node:latest diff --git a/package.json b/package.json index a3ba0ad..79c9d46 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build": "vite build", "test": "vite test", "format": "prettier --config .prettierrc 'front' --write", - "tsc": "node_modules/.bin/tsc" + "tsc": "tsc" }, "eslintConfig": { "extends": [ diff --git a/phpstan.neon b/phpstan.neon index 7801d9b..bc6c041 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,12 +4,10 @@ parameters: paths: - src - public - - sql - ignoreErrors: - - - message: '#.*#' - path: sql/database.php - - - message: '#.*#' - path: src/react-display-file.php - + scanFiles: + - config.php + - sql/database.php + - profiles/dev-config-profile.php + - profiles/prod-config-profile.php + excludePaths: + - src/react-display-file.php diff --git a/src/Controller/Control.php b/src/Controller/Control.php index e50eed0..2b428d9 100644 --- a/src/Controller/Control.php +++ b/src/Controller/Control.php @@ -14,7 +14,7 @@ class Control { /** * Runs given callback, if the request's json validates the given schema. * @param array $schema an array of `fieldName => Validators` which represents the request object schema - * @param callable $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. * @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead * of the ViewHttpResponse for an error view. @@ -38,7 +38,7 @@ class Control { * Runs given callback, if the given request data array validates the given schema. * @param array $data the request's data array. * @param array $schema an array of `fieldName => Validators` which represents the request object schema - * @param callable $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. * @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead * of the ViewHttpResponse for an error view. diff --git a/src/Controller/SampleFormController.php b/src/Controller/SampleFormController.php index 41b5f96..773a4db 100644 --- a/src/Controller/SampleFormController.php +++ b/src/Controller/SampleFormController.php @@ -31,7 +31,7 @@ class SampleFormController { /** * @param array $form - * @param callable $response + * @param callable(array>): ViewHttpResponse $response * @return HttpResponse */ private function submitForm(array $form, callable $response): HttpResponse { diff --git a/src/Data/Account.php b/src/Data/Account.php index 51d7aad..2a21bf1 100755 --- a/src/Data/Account.php +++ b/src/Data/Account.php @@ -29,7 +29,7 @@ class Account { private AccountUser $user; /** - * @var array account's teams + * @var Team[] account's teams */ private array $teams; @@ -43,7 +43,7 @@ class Account { * @param string $email * @param string $phoneNumber * @param AccountUser $user - * @param array $teams + * @param Team[] $teams * @param int $id */ public function __construct(string $email, string $phoneNumber, AccountUser $user, array $teams, int $id) { diff --git a/src/Data/Team.php b/src/Data/Team.php index a224bd4..aca4e0d 100755 --- a/src/Data/Team.php +++ b/src/Data/Team.php @@ -11,7 +11,7 @@ class Team { private Color $secondColor; /** - * @var array maps users with their role + * @var Member[] maps users with their role */ private array $members; @@ -20,7 +20,7 @@ class Team { * @param Url $picture * @param Color $mainColor * @param Color $secondColor - * @param array $members + * @param Member[] $members */ public function __construct(string $name, Url $picture, Color $mainColor, Color $secondColor, array $members) { $this->name = $name; @@ -59,7 +59,7 @@ class Team { } /** - * @return array + * @return Member[] */ public function listMembers(): array { return $this->members; diff --git a/src/Http/HttpRequest.php b/src/Http/HttpRequest.php index 6414d75..d199227 100644 --- a/src/Http/HttpRequest.php +++ b/src/Http/HttpRequest.php @@ -30,7 +30,7 @@ class HttpRequest implements ArrayAccess { * This is a simple function that only supports flat schemas (non-composed, the data must only be a k/v array pair.) * @param array $request the request's data * @param array $fails a reference to a failure array, that will contain the reported validation failures. - * @param array> $schema the schema to satisfy. a schema is a simple array with a string key (which is the top-level field name), and a set of validators + * @param array $schema the schema to satisfy. a schema is a simple array with a string key (which is the top-level field name), and a set of validators * @return HttpRequest|null the built HttpRequest instance, or null if a field is missing, or if any of the schema validator failed */ public static function from(array $request, array &$fails, array $schema): ?HttpRequest { @@ -65,7 +65,6 @@ class HttpRequest implements ArrayAccess { /** * @param $offset * @param $value - * @return mixed * @throws Exception */ public function offsetSet($offset, $value) { diff --git a/src/Validation/FunctionValidator.php b/src/Validation/FunctionValidator.php index f052ea6..4949d8b 100644 --- a/src/Validation/FunctionValidator.php +++ b/src/Validation/FunctionValidator.php @@ -4,12 +4,12 @@ namespace App\Validation; class FunctionValidator extends Validator { /** - * @var callable + * @var callable(string, mixed): ValidationFail[] */ private $validate_fn; /** - * @param callable $validate_fn the validate function. Must have the same signature as the {@link Validator::validate()} method. + * @param callable(string, mixed): ValidationFail[] $validate_fn the validate function. Must have the same signature as the {@link Validator::validate()} method. */ public function __construct(callable $validate_fn) { $this->validate_fn = $validate_fn; diff --git a/src/Validation/SimpleFunctionValidator.php b/src/Validation/SimpleFunctionValidator.php index 7514bae..cec52c0 100644 --- a/src/Validation/SimpleFunctionValidator.php +++ b/src/Validation/SimpleFunctionValidator.php @@ -7,17 +7,17 @@ namespace App\Validation; */ class SimpleFunctionValidator extends Validator { /** - * @var callable + * @var callable(mixed): bool */ private $predicate; /** - * @var callable + * @var callable(string): ValidationFail[] */ private $errorFactory; /** - * @param callable $predicate a function predicate with signature: `(string) => bool`, to validate the given string - * @param callable $errorsFactory a factory function with signature `(string) => array` to emit failures when the predicate fails + * @param callable(mixed): bool $predicate a function predicate with signature: `(string) => bool`, to validate the given string + * @param callable(string): ValidationFail[] $errorsFactory a factory function with signature `(string) => array` to emit failures when the predicate fails */ public function __construct(callable $predicate, callable $errorsFactory) { $this->predicate = $predicate; diff --git a/src/Validation/Validation.php b/src/Validation/Validation.php index 19144b7..f1392e6 100644 --- a/src/Validation/Validation.php +++ b/src/Validation/Validation.php @@ -10,7 +10,7 @@ class Validation { * Validate a value from validators, appending failures in the given errors array. * @param mixed $val the value to validate * @param string $valName the name of the value - * @param array $failures array to push when a validator fails + * @param ValidationFail[] $failures array to push when a validator fails * @param Validator ...$validators given validators * @return bool true if any of the given validators did fail */ diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index 4ad6131..8227e46 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -7,7 +7,7 @@ abstract class Validator { * validates a variable string * @param string $name the name of the tested value * @param mixed $val the value to validate - * @return array the errors the validator has reported + * @return ValidationFail[] the errors the validator has reported */ abstract public function validate(string $name, $val): array; diff --git a/src/Validation/Validators.php b/src/Validation/Validators.php index 6f72cf2..94aea23 100644 --- a/src/Validation/Validators.php +++ b/src/Validation/Validators.php @@ -9,7 +9,7 @@ class Validators { /** * @return Validator a validator that validates a given regex */ - public static function regex(string $regex, string $msg = null): Validator { + public static function regex(string $regex, ?string $msg = null): Validator { return new SimpleFunctionValidator( fn(string $str) => preg_match($regex, $str), fn(string $name) => [new FieldValidationFail($name, $msg == null ? "field does not validates pattern $regex" : $msg)] @@ -19,7 +19,7 @@ class Validators { /** * @return Validator a validator that validates strings that only contains numbers, letters, accents letters, `-` and `_`. */ - public static function name(string $msg = null): Validator { + public static function name(?string $msg = null): Validator { return self::regex("/^[0-9a-zA-Zà-üÀ-Ü_-]*$/", $msg); } From d47bf6bd7df24970fdf9fc3a2813c15e70c102f1 Mon Sep 17 00:00:00 2001 From: Vivien DUFOUR Date: Tue, 21 Nov 2023 10:40:15 +0100 Subject: [PATCH 25/75] visualizer/bootstrap (#14) Basic Visualizer for oral presentation Co-authored-by: vidufour1 Co-authored-by: vivien.dufour Reviewed-on: https://codefirst.iut.uca.fr/git/IQBall/Application-Web/pulls/14 --- front/style/visualizer.css | 30 +++++++++++++++++++++++ front/views/Visualizer.tsx | 24 +++++++++++++++++++ public/index.php | 3 +++ sql/.guard | 0 src/Controller/VisualizerController.php | 32 +++++++++++++++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 front/style/visualizer.css create mode 100644 front/views/Visualizer.tsx create mode 100644 sql/.guard create mode 100644 src/Controller/VisualizerController.php diff --git a/front/style/visualizer.css b/front/style/visualizer.css new file mode 100644 index 0000000..2d1a73f --- /dev/null +++ b/front/style/visualizer.css @@ -0,0 +1,30 @@ +#main { + height: 100vh; + width: 100%; + display: flex; + flex-direction: column; +} + +#topbar { + display: flex; + background-color: var(--main-color); + justify-content: center; + align-items: center; +} + +h1 { + text-align: center; + margin-top: 0; +} + +#court-container { + flex: 1; + display: flex; + justify-content: center; + background-color: var(--main-color); +} + +#court { + max-width: 80%; + max-height: 80%; +} diff --git a/front/views/Visualizer.tsx b/front/views/Visualizer.tsx new file mode 100644 index 0000000..ddf7fe2 --- /dev/null +++ b/front/views/Visualizer.tsx @@ -0,0 +1,24 @@ +import React, { CSSProperties, useState } from "react" +import "../style/visualizer.css" +import Court from "../assets/basketball_court.svg" + + +export default function Visualizer({id, name}: { id: number; name: string }) { + const [style, setStyle] = useState({}); + + return ( +
+
+

{name}

+
+
+ Basketball Court +
+
+ ); +} diff --git a/public/index.php b/public/index.php index e81d098..7f19f63 100644 --- a/public/index.php +++ b/public/index.php @@ -16,6 +16,7 @@ use App\Model\TacticModel; use Twig\Loader\FilesystemLoader; use App\Validation\ValidationFail; use App\Controller\ErrorController; +use App\Controller\VisualizerController; $loader = new FilesystemLoader('../src/Views/'); $twig = new \Twig\Environment($loader); @@ -29,6 +30,7 @@ $router->setBasePath($basePath); $sampleFormController = new SampleFormController(new FormResultGateway($con)); $editorController = new EditorController(new TacticModel(new TacticInfoGateway($con))); +$visualizerController = new VisualizerController(new TacticModel(new TacticInfoGateway($con))); $router->map("GET", "/", fn() => $sampleFormController->displayFormReact()); @@ -37,6 +39,7 @@ $router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); $router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); $router->map("GET", "/tactic/new", fn() => $editorController->makeNew()); $router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->openEditorFor($id)); +$router->map("GET", "/tactic/[i:id]", fn(int $id) => $visualizerController->openVisualizer($id)); $match = $router->match(); diff --git a/sql/.guard b/sql/.guard new file mode 100644 index 0000000..e69de29 diff --git a/src/Controller/VisualizerController.php b/src/Controller/VisualizerController.php new file mode 100644 index 0000000..3c8b55e --- /dev/null +++ b/src/Controller/VisualizerController.php @@ -0,0 +1,32 @@ +tacticModel = $tacticModel; + } + + public function openVisualizer(int $id): HttpResponse { + $tactic = $this->tacticModel->get($id); + + if ($tactic == null) { + return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND); + } + + return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]); + + } +} From 9333288705bfb0be8b20b8dc1771c15da95c1df2 Mon Sep 17 00:00:00 2001 From: Samuel BERION Date: Tue, 21 Nov 2023 11:13:56 +0100 Subject: [PATCH 26/75] Add register and login of authentification actions (#12) Co-authored-by: Samuel Co-authored-by: samuel Reviewed-on: https://codefirst.iut.uca.fr/git/IQBall/Application-Web/pulls/12 --- Documentation/models.puml | 26 +++++++ front/views/Visualizer.tsx | 7 +- public/index.php | 8 ++ sql/setup-tables.sql | 6 ++ src/Controller/AuthController.php | 94 ++++++++++++++++++++++++ src/Gateway/AuthGateway.php | 47 ++++++++++++ src/Model/AuthModel.php | 80 ++++++++++++++++++++ src/Views/display_auth_confirm.html.twig | 46 ++++++++++++ src/Views/display_login.html.twig | 85 +++++++++++++++++++++ src/Views/display_register.html.twig | 88 ++++++++++++++++++++++ src/Views/display_results.html.twig | 2 +- src/Views/sample_form.html.twig | 1 - 12 files changed, 484 insertions(+), 6 deletions(-) create mode 100644 src/Controller/AuthController.php create mode 100644 src/Gateway/AuthGateway.php create mode 100644 src/Model/AuthModel.php create mode 100644 src/Views/display_auth_confirm.html.twig create mode 100644 src/Views/display_login.html.twig create mode 100644 src/Views/display_register.html.twig diff --git a/Documentation/models.puml b/Documentation/models.puml index 1f4877a..d95343c 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -68,4 +68,30 @@ class Color { + getValue(): int } +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 + + getUserFields(email : string):array + + login(email : string, password : string) +} +AuthModel --> "- gateway" AuthGateway + +class AuthGateway{ + -con : Connection + + + mailExist(email : string) : bool + + insertAccount(username : string, hash : string, email : string) + + getUserHash(email : string):string + + getUserFields (email : string): array +} @enduml \ No newline at end of file diff --git a/front/views/Visualizer.tsx b/front/views/Visualizer.tsx index ddf7fe2..541da09 100644 --- a/front/views/Visualizer.tsx +++ b/front/views/Visualizer.tsx @@ -2,9 +2,8 @@ import React, { CSSProperties, useState } from "react" import "../style/visualizer.css" import Court from "../assets/basketball_court.svg" - -export default function Visualizer({id, name}: { id: number; name: string }) { - const [style, setStyle] = useState({}); +export default function Visualizer({ id, name }: { id: number; name: string }) { + const [style, setStyle] = useState({}) return (
@@ -20,5 +19,5 @@ export default function Visualizer({id, name}: { id: number; name: string }) { />
- ); + ) } diff --git a/public/index.php b/public/index.php index 7f19f63..edc4cb3 100644 --- a/public/index.php +++ b/public/index.php @@ -14,6 +14,8 @@ use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; use App\Model\TacticModel; use Twig\Loader\FilesystemLoader; +use App\Gateway\AuthGateway; +use App\Controller\AuthController; use App\Validation\ValidationFail; use App\Controller\ErrorController; use App\Controller\VisualizerController; @@ -29,6 +31,8 @@ $router = new AltoRouter(); $router->setBasePath($basePath); $sampleFormController = new SampleFormController(new FormResultGateway($con)); +$authGateway = new AuthGateway($con); +$authController = new \App\Controller\AuthController(new \App\Model\AuthModel($authGateway)); $editorController = new EditorController(new TacticModel(new TacticInfoGateway($con))); $visualizerController = new VisualizerController(new TacticModel(new TacticInfoGateway($con))); @@ -37,6 +41,10 @@ $router->map("GET", "/", fn() => $sampleFormController->displayFormReact()); $router->map("POST", "/submit", fn() => $sampleFormController->submitFormReact($_POST)); $router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); $router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); +$router->map("GET", "/register", fn() => $authController->displayRegister()); +$router->map("POST", "/register", fn() => $authController->confirmRegister($_POST)); +$router->map("GET", "/login", fn() => $authController->displayLogin()); +$router->map("POST", "/login", fn() => $authController->confirmLogin($_POST)); $router->map("GET", "/tactic/new", fn() => $editorController->makeNew()); $router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->openEditorFor($id)); $router->map("GET", "/tactic/[i:id]", fn(int $id) => $visualizerController->openVisualizer($id)); diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 068c2e1..108b62a 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -1,9 +1,15 @@ -- drop tables here DROP TABLE IF EXISTS FormEntries; +DROP TABLE IF EXISTS AccountUser; DROP TABLE IF EXISTS TacticInfo; CREATE TABLE FormEntries(name varchar, description varchar); +CREATE TABLE AccountUser( + username varchar, + hash varchar, + email varchar unique +); CREATE TABLE TacticInfo( id integer PRIMARY KEY AUTOINCREMENT, diff --git a/src/Controller/AuthController.php b/src/Controller/AuthController.php new file mode 100644 index 0000000..e42c27d --- /dev/null +++ b/src/Controller/AuthController.php @@ -0,0 +1,94 @@ +model = $model; + } + + public function displayRegister(): HttpResponse { + return ViewHttpResponse::twig("display_register.html.twig", []); + } + + /** + * @param string $viewName + * @param ValidationFail[] $fails + * @return HttpResponse + */ + private function displayBadFields(string $viewName, array $fails): HttpResponse { + $bad_fields = []; + foreach ($fails as $err) { + if ($err instanceof FieldValidationFail) { + $bad_fields[] = $err->getFieldName(); + } + } + return ViewHttpResponse::twig($viewName, ['bad_fields' => $bad_fields]); + } + + /** + * @param mixed[] $request + * @return HttpResponse + */ + public function confirmRegister(array $request): HttpResponse { + $fails = []; + $request = HttpRequest::from($request, $fails, [ + "username" => [Validators::name(), Validators::lenBetween(2, 32)], + "password" => [Validators::lenBetween(6, 256)], + "confirmpassword" => [Validators::lenBetween(6, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)], + ]); + if (!empty($fails)) { + return $this->displayBadFields("display_register.html.twig", $fails); + } + $fails = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email']); + if (empty($fails)) { + $results = $this->model->getUserFields($request['email']); + return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]); + } + return $this->displayBadFields("display_register.html.twig", $fails); + } + + + public function displayLogin(): HttpResponse { + return ViewHttpResponse::twig("display_login.html.twig", []); + } + + /** + * @param mixed[] $request + * @return HttpResponse + */ + public function confirmLogin(array $request): HttpResponse { + $fails = []; + $request = HttpRequest::from($request, $fails, [ + "password" => [Validators::lenBetween(6, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)], + ]); + if (!empty($fails)) { + return $this->displayBadFields("display_login.html.twig", $fails); + } + + $fails = $this->model->login($request['email'], $request['password']); + if (empty($fails)) { + $results = $this->model->getUserFields($request['email']); + return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]); + } + return $this->displayBadFields("display_login.html.twig", $fails); + } + +} diff --git a/src/Gateway/AuthGateway.php b/src/Gateway/AuthGateway.php new file mode 100644 index 0000000..5acc01c --- /dev/null +++ b/src/Gateway/AuthGateway.php @@ -0,0 +1,47 @@ +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 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|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; + } + + + + +} diff --git a/src/Model/AuthModel.php b/src/Model/AuthModel.php new file mode 100644 index 0000000..45b63e4 --- /dev/null +++ b/src/Model/AuthModel.php @@ -0,0 +1,80 @@ +gateway = $gateway; + } + + + /** + * @param string $username + * @param string $password + * @param string $confirmPassword + * @param string $email + * @return ValidationFail[] + */ + public function register(string $username, string $password, string $confirmPassword, string $email): array { + $errors = []; + + if ($password != $confirmPassword) { + $errors[] = new FieldValidationFail("confirmpassword", "password and password confirmation are not equals"); + } + + if ($this->gateway->mailExist($email)) { + $errors[] = new FieldValidationFail("email", "email already exist"); + } + + if(empty($errors)) { + $hash = password_hash($password, PASSWORD_DEFAULT); + $this->gateway->insertAccount($username, $hash, $email); + } + + return $errors; + } + + /** + * @param string $email + * @return array|null + */ + public function getUserFields(string $email): ?array { + return $this->gateway->getUserFields($email); + } + + + /** + * @param string $email + * @param string $password + * @return ValidationFail[] $errors + */ + public function login(string $email, string $password): array { + $errors = []; + + if (!$this->gateway->mailExist($email)) { + $errors[] = new FieldValidationFail("email", "email doesnt exists"); + return $errors; + } + $hash = $this->gateway->getUserHash($email); + + if (!password_verify($password, $hash)) { + $errors[] = new FieldValidationFail("password", "invalid password"); + } + + return $errors; + } + + + + + +} diff --git a/src/Views/display_auth_confirm.html.twig b/src/Views/display_auth_confirm.html.twig new file mode 100644 index 0000000..60c63b2 --- /dev/null +++ b/src/Views/display_auth_confirm.html.twig @@ -0,0 +1,46 @@ + + + + + Profil Utilisateur + + + + + + + + \ No newline at end of file diff --git a/src/Views/display_login.html.twig b/src/Views/display_login.html.twig new file mode 100644 index 0000000..33b2385 --- /dev/null +++ b/src/Views/display_login.html.twig @@ -0,0 +1,85 @@ + + + + Connexion + + + + + +
+

Se connecter

+
+
+ + + + + + +
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/src/Views/display_register.html.twig b/src/Views/display_register.html.twig new file mode 100644 index 0000000..40199a0 --- /dev/null +++ b/src/Views/display_register.html.twig @@ -0,0 +1,88 @@ + + + + S'enregistrer + + + + + +
+

S'enregistrer

+
+
+ + + + + + + + + +
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/src/Views/display_results.html.twig b/src/Views/display_results.html.twig index 6d2aef0..a33546b 100644 --- a/src/Views/display_results.html.twig +++ b/src/Views/display_results.html.twig @@ -1,4 +1,3 @@ - @@ -14,5 +13,6 @@

description: {{ v.description }}

{% endfor %} + \ No newline at end of file diff --git a/src/Views/sample_form.html.twig b/src/Views/sample_form.html.twig index bcb958e..6f4a9b5 100644 --- a/src/Views/sample_form.html.twig +++ b/src/Views/sample_form.html.twig @@ -1,4 +1,3 @@ - From d824f17ea75f277d3a70b90c21eaf839c8dc861d Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Tue, 21 Nov 2023 11:36:25 +0100 Subject: [PATCH 27/75] every bugs has been fixed --- .gitignore | 2 +- sql/setup-tables.sql | 12 ++++++------ src/Controller/TeamController.php | 6 ++---- src/Data/Color.php | 22 +++++++++++----------- src/Data/Team.php | 12 +++++------- src/Gateway/TeamGateway.php | 10 +++++----- src/Model/TeamModel.php | 17 ++++++----------- src/Validation/Validators.php | 1 - src/Views/display_team.html.twig | 19 +++++++++++++++++-- src/Views/display_teams.html.twig | 2 +- 10 files changed, 54 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index 9124809..b116c4f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ .vite vendor - +.nfs* composer.lock *.phar /dist diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 1366a87..4b9f10b 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -2,8 +2,8 @@ DROP TABLE IF EXISTS FormEntries; DROP TABLE IF EXISTS TacticInfo; DROP TABLE IF EXISTS Team; +DROP TABLE IF EXISTS User; DROP TABLE IF EXISTS Member; -DROP TABLE IF EXISTS Participate; CREATE TABLE FormEntries(name varchar, description varchar); @@ -11,20 +11,20 @@ CREATE TABLE Team( id integer PRIMARY KEY AUTOINCREMENT, name varchar, picture varchar, - mainColor numeric, - secondColor numeric + mainColor varchar, + secondColor varchar ); -CREATE TABLE Member( +CREATE TABLE User( id integer PRIMARY KEY AUTOINCREMENT ); -CREATE TABLE Participate( +CREATE TABLE Member( idTeam integer, idMember integer, role char(1) CHECK (role IN ('C','P')), FOREIGN KEY (idTeam) REFERENCES Team(id), - FOREIGN KEY (idMember) REFERENCES Member(id) + FOREIGN KEY (idMember) REFERENCES User(id) ); CREATE TABLE TacticInfo( diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index 30a6ab6..fa418c5 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -31,13 +31,11 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ public function submitTeam(array $request): HttpResponse { $errors = []; - $request['mainColor'] = hexdec($request['mainColor']); - $request['secondColor'] = hexdec($request['secondColor']); $request = HttpRequest::from($request, $errors, [ "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], - "mainColor" => [Validators::isInteger(), Validators::isIntInRange(0, 0xffffff)], - "secondColor" => [Validators::isInteger(), Validators::isIntInRange(0, 0xffffff)], + "mainColor" => [Validators::regex('/#(?:[0-9a-fA-F]{6})/')], + "secondColor" => [Validators::regex('/#(?:[0-9a-fA-F]{6})/')], "picture" => [Validators::isURL()] ]); if (!empty($errors)) { diff --git a/src/Data/Color.php b/src/Data/Color.php index bc9043b..2e934c0 100755 --- a/src/Data/Color.php +++ b/src/Data/Color.php @@ -2,39 +2,39 @@ namespace App\Data; -use http\Exception\InvalidArgumentException; +use \InvalidArgumentException; class Color { /** - * @var int 6 bytes unsigned int that represents an RGB color + * @var string 6 bytes unsigned int that represents an RGB color */ - private int $value; + private string $hex; /** - * @param int $value 6 bytes unsigned int that represents an RGB color + * @param string $value 6 bytes unsigned int that represents an RGB color * @throws \InvalidArgumentException if the value is negative or greater than 0xFFFFFF */ - private function __constructor(int $value) { + private function __construct(string $value) { $this->value = $value; } /** - * @return int + * @return string */ - public function getValue(): int { + public function getValue(): string { return $this->value; } - public static function from(int $value): Color { + public static function from(string $value): Color { $color = self::tryFrom($value); if ($color == null) { - throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF"); + throw new InvalidArgumentException("The string is not an hexadecimal code"); } return $color; } - public static function tryFrom(int $value): ?Color { - if ($value < 0 || $value > 0xFFFFFF) { + public static function tryFrom(string $value): ?Color { + if (!preg_match('/#(?:[0-9a-fA-F]{6})/',$value)) { return null; } return new Color($value); diff --git a/src/Data/Team.php b/src/Data/Team.php index 321a5d9..4c80dc6 100755 --- a/src/Data/Team.php +++ b/src/Data/Team.php @@ -2,12 +2,10 @@ namespace App\Data; -use http\Url; - class Team { private int $id; private string $name; - private Url $picture; + private string $picture; private Color $mainColor; private Color $secondColor; @@ -18,12 +16,12 @@ class Team { /** * @param string $name - * @param Url $picture + * @param string $picture * @param Color $mainColor * @param Color $secondColor * @param array $members */ - public function __construct(int $id,string $name, Url $picture, Color $mainColor, Color $secondColor, array $members =[]) { + public function __construct(int $id,string $name, string $picture, Color $mainColor, Color $secondColor, array $members =[]) { $this->id = $id; $this->name = $name; $this->picture = $picture; @@ -47,9 +45,9 @@ class Team { } /** - * @return Url + * @return string */ - public function getPicture(): Url { + public function getPicture(): string { return $this->picture; } diff --git a/src/Gateway/TeamGateway.php b/src/Gateway/TeamGateway.php index 4bb1518..1cecbe1 100644 --- a/src/Gateway/TeamGateway.php +++ b/src/Gateway/TeamGateway.php @@ -12,14 +12,14 @@ class TeamGateway { $this->con = $con; } - public function insert(string $name, string $picture, int $mainColor, int $secondColor) { + public function insert(string $name, string $picture, string $mainColor, string $secondColor) { $this->con->exec( "INSERT INTO Team(name, picture, mainColor, secondColor) VALUES (:teamName , :picture, :mainColor, :secondColor)", [ ":teamName" => [$name, PDO::PARAM_STR], ":picture" => [$picture, PDO::PARAM_STR], - ":mainColor" => [$mainColor, PDO::PARAM_INT], - ":secondColor" => [$secondColor, PDO::PARAM_INT] + ":mainColor" => [$mainColor, PDO::PARAM_STR], + ":secondColor" => [$secondColor, PDO::PARAM_STR] ] ); } @@ -35,7 +35,7 @@ class TeamGateway { public function getTeamById(int $id): array{ return $this->con->fetch( - "SELECT name,picture,mainColor,secondColor FROM Team WHERE id = :id", + "SELECT id,name,picture,mainColor,secondColor FROM Team WHERE id = :id", [ ":id" => [$id, PDO::PARAM_INT] ] @@ -53,7 +53,7 @@ class TeamGateway { public function getMembersById($id):array{ return $this->con->fetch( - "SELECT p.role,m.email,m.id FROM Member m,Team t,Participate p WHERE t.id = :id AND p.idTeam = t.id AND p.idMember = m.id", + "SELECT m.role,u.id FROM User u,Team t,Member m WHERE t.id = :id AND m.idTeam = t.id AND m.idMember = u.id", [ ":id" => [$id, PDO::PARAM_INT] ] diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php index 1dd57a0..c5521eb 100644 --- a/src/Model/TeamModel.php +++ b/src/Model/TeamModel.php @@ -6,12 +6,9 @@ use App\Gateway\TeamGateway; use App\Data\Team; use App\Data\Member; use App\Data\MemberRole; -use http\Url; +use App\Data\Color; -/** - * - */ -class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) pour le controller qui l'utilise, catch celle de la gw, */ +class TeamModel { private TeamGateway $gateway; @@ -22,7 +19,7 @@ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) po $this->gateway = $gateway; } - public function createTeam(string $name, string $picture, int $mainColor, int $secondColor): int{ + public function createTeam(string $name, string $picture, string $mainColor, string $secondColor): int { $this->gateway->insert($name, $picture, $mainColor, $secondColor); $result = $this->gateway->getIdTeamByName($name); return intval($result[0]['id']); @@ -32,15 +29,14 @@ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) po $teams = []; $results = $this->gateway->listByName($name); foreach ($results as $row) { - $url = new Url($row['picture']); - $teams[] = new Team($row['id'], $row['name'], $url, $row['mainColor'], $row['secondColor']); + $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 = []; - $result = $this->gateway->getTeamById($id); + $result = $this->gateway->getTeamById($id)[0]; $resultMembers = $this->gateway->getMembersById($id); foreach ($resultMembers as $row) { if ($row['role'] == 'C') { @@ -50,7 +46,6 @@ class TeamModel /* throw des exceptions(ex validation des champs, filtre etc) po } $members[] = new Member($row['id'], $role); } - return new Team($result['id'], $result['name'], $result['picture'], $result['mainColor'], $result['secondColor'], $members); + return new Team(intval($result['id']), $result['name'], $result['picture'], Color::from($result['mainColor']), Color::from($result['secondColor']), $members); } - } \ No newline at end of file diff --git a/src/Validation/Validators.php b/src/Validation/Validators.php index 2f69ca4..a36bbef 100644 --- a/src/Validation/Validators.php +++ b/src/Validation/Validators.php @@ -69,5 +69,4 @@ class Validators { fn(string $name) => [new FieldValidationFail($name, "The value is not an URL")] ); } - } \ No newline at end of file diff --git a/src/Views/display_team.html.twig b/src/Views/display_team.html.twig index d06322f..e3b0fe0 100644 --- a/src/Views/display_team.html.twig +++ b/src/Views/display_team.html.twig @@ -3,14 +3,29 @@ Twig view +

{{ team.name }}

- +Logo d'équipe +
+
{% for m in team.members %} -

m.email

+

m.id

{% endfor %} diff --git a/src/Views/display_teams.html.twig b/src/Views/display_teams.html.twig index e4941e4..c0ac185 100644 --- a/src/Views/display_teams.html.twig +++ b/src/Views/display_teams.html.twig @@ -24,7 +24,7 @@ {% for t in teams %}

Nom de l'équipe : {{ t.name }}

-

picture : {{ t.picture }}

+ logo de l'équipe
{% endfor %} {% endif %} From 53584a518e44200be3f47bfc99cfc6191fab300f Mon Sep 17 00:00:00 2001 From: Override-6 Date: Tue, 21 Nov 2023 22:57:36 +0100 Subject: [PATCH 28/75] add salva-like routes and controller architecture --- src/Controller/ErrorController.php | 26 -------- src/Controller/FrontController.php | 2 +- src/Controller/SampleFormController.php | 64 ------------------- src/Controller/{ => Sub}/AuthController.php | 4 +- src/Controller/{ => Sub}/EditorController.php | 4 +- .../{ => Sub}/VisualizerController.php | 5 +- src/Controller/UserController.php | 36 ++++++++++- src/Model/AuthModel.php | 1 - 8 files changed, 40 insertions(+), 102 deletions(-) delete mode 100644 src/Controller/ErrorController.php delete mode 100644 src/Controller/SampleFormController.php rename src/Controller/{ => Sub}/AuthController.php (97%) rename src/Controller/{ => Sub}/EditorController.php (93%) rename src/Controller/{ => Sub}/VisualizerController.php (88%) diff --git a/src/Controller/ErrorController.php b/src/Controller/ErrorController.php deleted file mode 100644 index 7fc5239..0000000 --- a/src/Controller/ErrorController.php +++ /dev/null @@ -1,26 +0,0 @@ -display("error.html.twig", ['failures' => $failures]); - } catch (LoaderError|RuntimeError|SyntaxError $e) { - echo "Twig error: $e"; - } - } -} diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index 66d5d4f..0600a01 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -56,7 +56,7 @@ class FrontController { private function initializeRouterMap(): void { $this->router->map("GET", "/", "UserController"); $this->router->map("GET", "/[a:action]?", "UserController"); - $this->router->map("GET", "/tactic/[a:action]/[i:idTactic]?", "EditorController"); + $this->router->map("GET", "/tactic/[a:action]/[i:idTactic]?", "UserController"); } /** diff --git a/src/Controller/SampleFormController.php b/src/Controller/SampleFormController.php deleted file mode 100644 index 773a4db..0000000 --- a/src/Controller/SampleFormController.php +++ /dev/null @@ -1,64 +0,0 @@ -gateway = $gateway; - } - - - public function displayFormReact(): HttpResponse { - return ViewHttpResponse::react("views/SampleForm.tsx", []); - } - - public function displayFormTwig(): HttpResponse { - return ViewHttpResponse::twig('sample_form.html.twig', []); - } - - /** - * @param array $form - * @param callable(array>): ViewHttpResponse $response - * @return HttpResponse - */ - private function submitForm(array $form, callable $response): HttpResponse { - return Control::runCheckedFrom($form, [ - "name" => [Validators::lenBetween(0, 32), Validators::name("Le nom ne peut contenir que des lettres, des chiffres et des accents")], - "description" => [Validators::lenBetween(0, 512)], - ], function (HttpRequest $req) use ($response) { - $description = htmlspecialchars($req["description"]); - $this->gateway->insert($req["name"], $description); - $results = ["results" => $this->gateway->listResults()]; - return call_user_func_array($response, [$results]); - }, false); - } - - /** - * @param array $form - * @return HttpResponse - */ - public function submitFormTwig(array $form): HttpResponse { - return $this->submitForm($form, fn(array $results) => ViewHttpResponse::twig('display_results.html.twig', $results)); - } - - /** - * @param array $form - * @return HttpResponse - */ - public function submitFormReact(array $form): HttpResponse { - return $this->submitForm($form, fn(array $results) => ViewHttpResponse::react('views/DisplayResults.tsx', $results)); - } -} diff --git a/src/Controller/AuthController.php b/src/Controller/Sub/AuthController.php similarity index 97% rename from src/Controller/AuthController.php rename to src/Controller/Sub/AuthController.php index e42c27d..a78b02e 100644 --- a/src/Controller/AuthController.php +++ b/src/Controller/Sub/AuthController.php @@ -1,8 +1,7 @@ $tactic->getName(), "id" => $tactic->getId()]); } - public function create(): HttpResponse { + public function createNew(): HttpResponse { $tactic = $this->model->makeNewDefault(); return $this->openEditor($tactic); } diff --git a/src/Controller/VisualizerController.php b/src/Controller/Sub/VisualizerController.php similarity index 88% rename from src/Controller/VisualizerController.php rename to src/Controller/Sub/VisualizerController.php index 3c8b55e..e3b5663 100644 --- a/src/Controller/VisualizerController.php +++ b/src/Controller/Sub/VisualizerController.php @@ -1,6 +1,6 @@ tacticModel = $tacticModel; } - public function openVisualizer(int $id): HttpResponse { + public function visualize(int $id): HttpResponse { $tactic = $this->tacticModel->get($id); if ($tactic == null) { @@ -27,6 +27,5 @@ class VisualizerController { } return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]); - } } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 930cd67..2a95bcc 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -2,15 +2,47 @@ namespace App\Controller; +use App\Connexion; +use App\Gateway\AuthGateway; +use App\Gateway\TacticInfoGateway; use App\Http\HttpResponse; use App\Http\ViewHttpResponse; +use App\Model\AuthModel; +use App\Model\TacticModel; class UserController { public function home(): HttpResponse { return ViewHttpResponse::twig("home.twig", []); } - public function default(): HttpResponse { - return self::home(); + public function register(): HttpResponse { + $model = new AuthModel(new AuthGateway(new Connexion(get_database()))); + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + return (new Sub\AuthController($model))->displayRegister(); + } + return (new Sub\AuthController($model))->confirmRegister($_POST); + } + + public function login(): HttpResponse { + $model = new AuthModel(new AuthGateway(new Connexion(get_database()))); + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + return (new Sub\AuthController($model))->displayLogin(); + } + return (new Sub\AuthController($model))->confirmLogin($_POST); + } + + public function open(int $id): HttpResponse { + $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); + return (new Sub\VisualizerController($model))->visualize($id); + } + + public function edit(int $id): HttpResponse { + $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); + return (new Sub\EditorController($model))->edit($id); + } + + public function create(): HttpResponse { + $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); + return (new Sub\EditorController($model))->createNew(); } } diff --git a/src/Model/AuthModel.php b/src/Model/AuthModel.php index 45b63e4..312cca8 100644 --- a/src/Model/AuthModel.php +++ b/src/Model/AuthModel.php @@ -2,7 +2,6 @@ namespace App\Model; -use App\Controller\AuthController; use App\Gateway\AuthGateway; use App\Validation\FieldValidationFail; use App\Validation\ValidationFail; From 14612226cd1f9680776ad2de295a6e0afbc88b8f Mon Sep 17 00:00:00 2001 From: "vivien.dufour" Date: Wed, 22 Nov 2023 08:38:58 +0100 Subject: [PATCH 29/75] fix on routes --- src/Controller/FrontController.php | 49 ++++++++++++++----------- src/Controller/Sub/EditorController.php | 4 +- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index 0600a01..58e5d9d 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -14,10 +14,12 @@ use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Loader\FilesystemLoader; -class FrontController { +class FrontController +{ private AltoRouter $router; - public function __construct(string $basePath) { + public function __construct(string $basePath) + { $this->router = $this->createRouter($basePath); $this->initializeRouterMap(); } @@ -27,7 +29,8 @@ class FrontController { * * @return void */ - public function run(): void { + public function run(): void + { $match = $this->router->match(); if ($match != null) { $this->handleMatch($match); @@ -42,7 +45,8 @@ class FrontController { * @param string $basePath * @return AltoRouter */ - public function createRouter(string $basePath): AltoRouter { + public function createRouter(string $basePath): AltoRouter + { $router = new AltoRouter(); $router->setBasePath($basePath); return $router; @@ -53,17 +57,19 @@ class FrontController { * * @return void */ - private function initializeRouterMap(): void { + private function initializeRouterMap(): void + { $this->router->map("GET", "/", "UserController"); - $this->router->map("GET", "/[a:action]?", "UserController"); - $this->router->map("GET", "/tactic/[a:action]/[i:idTactic]?", "UserController"); + $this->router->map("GET|POST", "/[a:action]?", "UserController"); + $this->router->map("GET|POST", "/tactic/[a:action]/[i:idTactic]?", "UserController"); } /** * @param array $match * @return void */ - private function handleMatch(array $match): void { + private function handleMatch(array $match): void + { $tag = $match['target']; $action = $this->getAction($match); @@ -78,15 +84,12 @@ class FrontController { * @param array $params * @return HttpResponse */ - private function tryToCall(string $controller, string $action, array $params): HttpResponse { + private function tryToCall(string $controller, string $action, array $params): HttpResponse + { $controller = $this->getController($controller); - try { - if (is_callable([$controller, $action])) { - return call_user_func_array([$controller, $action], $params); - } else { - return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); - } - } catch (Exception $e) { + if (is_callable([$controller, $action])) { + return call_user_func_array([$controller, $action], $params); + } else { return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); } } @@ -97,7 +100,8 @@ class FrontController { * @param array $match * @return string */ - private function getAction(array $match): string { + private function getAction(array $match): string + { if (isset($match["params"]["action"])) { return $match["params"]["action"]; } @@ -110,7 +114,8 @@ class FrontController { * @param string $controller * @return mixed */ - private function getController(string $controller) { + private function getController(string $controller) + { $namespace = "\\App\\Controller\\"; $controller = $namespace . $controller; return new $controller(); @@ -122,7 +127,8 @@ class FrontController { * @param HttpResponse $response * @return void */ - private function handleResponseByType(HttpResponse $response): void { + private function handleResponseByType(HttpResponse $response): void + { http_response_code($response->getCode()); if ($response instanceof ViewHttpResponse) { $this->displayViewByKind($response); @@ -138,7 +144,8 @@ class FrontController { * @param ViewHttpResponse $response * @return void */ - private function displayViewByKind(ViewHttpResponse $response): void { + private function displayViewByKind(ViewHttpResponse $response): void + { $file = $response->getFile(); $args = $response->getArguments(); @@ -151,7 +158,7 @@ class FrontController { $loader = new FilesystemLoader('../src/Views/'); $twig = new Environment($loader); $twig->display($file, $args); - } catch (RuntimeError|SyntaxError|LoaderError $e) { + } catch (RuntimeError | SyntaxError | LoaderError $e) { http_response_code(500); echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); throw $e; diff --git a/src/Controller/Sub/EditorController.php b/src/Controller/Sub/EditorController.php index def6ac9..ce7a0a3 100644 --- a/src/Controller/Sub/EditorController.php +++ b/src/Controller/Sub/EditorController.php @@ -14,8 +14,8 @@ use App\Model\TacticModel; class EditorController { private TacticModel $model; - public function __construct() { - $this->model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); + public function __construct(TacticModel $model) { + $this->model = $model; } private function openEditor(TacticInfo $tactic): HttpResponse { From 3d7eb7bbb10eb1a0012ed531dc19f760addf7af0 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Tue, 21 Nov 2023 19:40:30 +0100 Subject: [PATCH 30/75] add session handle --- Documentation/models.puml | 8 +-- public/index.php | 5 +- sql/setup-tables.sql | 24 +++++---- src/Controller/FrontController.php | 52 +++++++++---------- src/Controller/Sub/AuthController.php | 24 ++++----- src/Controller/UserController.php | 6 +-- src/Data/Account.php | 73 ++++----------------------- src/Gateway/AccountGateway.php | 56 ++++++++++++++++++++ src/Gateway/AuthGateway.php | 47 ----------------- src/Model/AuthModel.php | 66 ++++++++++++------------ src/Session/MutableSessionHandle.php | 9 ++++ src/Session/PhpSessionHandle.php | 24 +++++++++ src/Session/SessionHandle.php | 11 ++++ 13 files changed, 206 insertions(+), 199 deletions(-) create mode 100644 src/Gateway/AccountGateway.php delete mode 100644 src/Gateway/AuthGateway.php create mode 100644 src/Session/MutableSessionHandle.php create mode 100644 src/Session/PhpSessionHandle.php create mode 100644 src/Session/SessionHandle.php diff --git a/Documentation/models.puml b/Documentation/models.puml index d95343c..82037bc 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -81,7 +81,7 @@ AuthController --> "- model" AuthModel class AuthModel{ + register(username : string, password : string, confirmPassword : string, email : string): array - + getUserFields(email : string):array + + getAccount(email : string):array + login(email : string, password : string) } AuthModel --> "- gateway" AuthGateway @@ -89,9 +89,9 @@ AuthModel --> "- gateway" AuthGateway class AuthGateway{ -con : Connection - + mailExist(email : string) : bool + + mailExists(email : string) : bool + insertAccount(username : string, hash : string, email : string) - + getUserHash(email : string):string - + getUserFields (email : string): array + + getHash(email : string):string + + getAccount (email : string): array } @enduml \ No newline at end of file diff --git a/public/index.php b/public/index.php index f898d7f..ce1df33 100644 --- a/public/index.php +++ b/public/index.php @@ -1,5 +1,6 @@ run(); + +$frontController->run(PhpSessionHandle::init()); diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 108b62a..a289336 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -1,18 +1,24 @@ - -- drop tables here DROP TABLE IF EXISTS FormEntries; -DROP TABLE IF EXISTS AccountUser; +DROP TABLE IF EXISTS Account; DROP TABLE IF EXISTS TacticInfo; -CREATE TABLE FormEntries(name varchar, description varchar); -CREATE TABLE AccountUser( +CREATE TABLE FormEntries +( + name varchar, + description varchar +); +CREATE TABLE Account +( username varchar, - hash varchar, - email varchar unique + hash varchar, + email varchar unique, + token varchar(256) NOT NULL UNIQUE ); -CREATE TABLE TacticInfo( - id integer PRIMARY KEY AUTOINCREMENT, - name varchar, +CREATE TABLE TacticInfo +( + id integer PRIMARY KEY AUTOINCREMENT, + name varchar, creation_date timestamp DEFAULT CURRENT_TIMESTAMP ); \ No newline at end of file diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index 58e5d9d..d5c6417 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -7,6 +7,7 @@ use App\Http\HttpCodes; use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; +use App\Session\MutableSessionHandle; use Exception; use Twig\Environment; use Twig\Error\LoaderError; @@ -14,12 +15,10 @@ use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Loader\FilesystemLoader; -class FrontController -{ +class FrontController { private AltoRouter $router; - public function __construct(string $basePath) - { + public function __construct(string $basePath) { $this->router = $this->createRouter($basePath); $this->initializeRouterMap(); } @@ -29,11 +28,10 @@ class FrontController * * @return void */ - public function run(): void - { + public function run(MutableSessionHandle $session): void { $match = $this->router->match(); if ($match != null) { - $this->handleMatch($match); + $this->handleMatch($match, $session); } else { $this->displayViewByKind(ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND)); } @@ -45,8 +43,7 @@ class FrontController * @param string $basePath * @return AltoRouter */ - public function createRouter(string $basePath): AltoRouter - { + public function createRouter(string $basePath): AltoRouter { $router = new AltoRouter(); $router->setBasePath($basePath); return $router; @@ -57,8 +54,7 @@ class FrontController * * @return void */ - private function initializeRouterMap(): void - { + private function initializeRouterMap(): void { $this->router->map("GET", "/", "UserController"); $this->router->map("GET|POST", "/[a:action]?", "UserController"); $this->router->map("GET|POST", "/tactic/[a:action]/[i:idTactic]?", "UserController"); @@ -68,28 +64,34 @@ class FrontController * @param array $match * @return void */ - private function handleMatch(array $match): void - { + private function handleMatch(array $match, MutableSessionHandle $session): void { $tag = $match['target']; $action = $this->getAction($match); $params = $match["params"]; unset($params['action']); - $this->handleResponseByType($this->tryToCall($tag, $action, array_values($params))); + $this->handleResponseByType($this->tryToCall($tag, $action, array_values($params), $session)); } /** * @param string $controller * @param string $action * @param array $params + * @param MutableSessionHandle $session * @return HttpResponse */ - private function tryToCall(string $controller, string $action, array $params): HttpResponse - { + + private function tryToCall(string $controller, string $action, array $params, MutableSessionHandle $session): HttpResponse { $controller = $this->getController($controller); - if (is_callable([$controller, $action])) { - return call_user_func_array([$controller, $action], $params); - } else { + try { + if (is_callable([$controller, $action])) { + // append the session as the last parameter of a controller function + $params[] = $session; + return call_user_func_array([$controller, $action], $params); + } else { + return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); + } + } catch (Exception $e) { return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); } } @@ -100,8 +102,7 @@ class FrontController * @param array $match * @return string */ - private function getAction(array $match): string - { + private function getAction(array $match): string { if (isset($match["params"]["action"])) { return $match["params"]["action"]; } @@ -114,8 +115,7 @@ class FrontController * @param string $controller * @return mixed */ - private function getController(string $controller) - { + private function getController(string $controller) { $namespace = "\\App\\Controller\\"; $controller = $namespace . $controller; return new $controller(); @@ -127,8 +127,7 @@ class FrontController * @param HttpResponse $response * @return void */ - private function handleResponseByType(HttpResponse $response): void - { + private function handleResponseByType(HttpResponse $response): void { http_response_code($response->getCode()); if ($response instanceof ViewHttpResponse) { $this->displayViewByKind($response); @@ -144,8 +143,7 @@ class FrontController * @param ViewHttpResponse $response * @return void */ - private function displayViewByKind(ViewHttpResponse $response): void - { + private function displayViewByKind(ViewHttpResponse $response): void { $file = $response->getFile(); $args = $response->getArguments(); diff --git a/src/Controller/Sub/AuthController.php b/src/Controller/Sub/AuthController.php index a78b02e..7a537f2 100644 --- a/src/Controller/Sub/AuthController.php +++ b/src/Controller/Sub/AuthController.php @@ -2,6 +2,7 @@ namespace App\Controller\Sub; +use App\Gateway\AccountGateway; use App\Http\HttpRequest; use App\Http\HttpResponse; use App\Http\ViewHttpResponse; @@ -49,17 +50,16 @@ class AuthController { "username" => [Validators::name(), Validators::lenBetween(2, 32)], "password" => [Validators::lenBetween(6, 256)], "confirmpassword" => [Validators::lenBetween(6, 256)], - "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)], ]); if (!empty($fails)) { return $this->displayBadFields("display_register.html.twig", $fails); } - $fails = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email']); - if (empty($fails)) { - $results = $this->model->getUserFields($request['email']); - return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]); + $account = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email'], $fails); + if (!empty($fails)) { + return $this->displayBadFields("display_register.html.twig", $fails); } - return $this->displayBadFields("display_register.html.twig", $fails); + return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $account->getName(), 'email' => $account->getEmail()]); } @@ -75,18 +75,18 @@ class AuthController { $fails = []; $request = HttpRequest::from($request, $fails, [ "password" => [Validators::lenBetween(6, 256)], - "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)], ]); if (!empty($fails)) { return $this->displayBadFields("display_login.html.twig", $fails); } - $fails = $this->model->login($request['email'], $request['password']); - if (empty($fails)) { - $results = $this->model->getUserFields($request['email']); - return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]); + $account = $this->model->login($request['email'], $request['password'], $fails); + if (!empty($fails)) { + return $this->displayBadFields("display_login.html.twig", $fails); } - return $this->displayBadFields("display_login.html.twig", $fails); + + return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $account->getName(), 'email' => $account->getEmail()]); } } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 2a95bcc..360c9a3 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -3,7 +3,7 @@ namespace App\Controller; use App\Connexion; -use App\Gateway\AuthGateway; +use App\Gateway\AccountGateway; use App\Gateway\TacticInfoGateway; use App\Http\HttpResponse; use App\Http\ViewHttpResponse; @@ -16,7 +16,7 @@ class UserController { } public function register(): HttpResponse { - $model = new AuthModel(new AuthGateway(new Connexion(get_database()))); + $model = new AuthModel(new AccountGateway(new Connexion(get_database()))); if ($_SERVER['REQUEST_METHOD'] === 'GET') { return (new Sub\AuthController($model))->displayRegister(); } @@ -24,7 +24,7 @@ class UserController { } public function login(): HttpResponse { - $model = new AuthModel(new AuthGateway(new Connexion(get_database()))); + $model = new AuthModel(new AccountGateway(new Connexion(get_database()))); if ($_SERVER['REQUEST_METHOD'] === 'GET') { return (new Sub\AuthController($model))->displayLogin(); } diff --git a/src/Data/Account.php b/src/Data/Account.php index 2a21bf1..8b58e1c 100755 --- a/src/Data/Account.php +++ b/src/Data/Account.php @@ -4,8 +4,6 @@ namespace App\Data; use http\Exception\InvalidArgumentException; -const PHONE_NUMBER_REGEXP = "/^\\+[0-9]+$/"; - /** * Base class of a user account. * Contains the private information that we don't want @@ -16,27 +14,16 @@ class Account { * @var string $email account's mail address */ private string $email; - /** - * @var string account's phone number. - * its format is specified by the {@link PHONE_NUMBER_REGEXP} constant - * - */ - private string $phoneNumber; /** - * @var AccountUser account's public and shared information + * @var string string token */ - private AccountUser $user; + private string $token; /** - * @var Team[] account's teams + * @var string the account's username */ - private array $teams; - - /** - * @var int account's unique identifier - */ - private int $id; + private string $name; /** @@ -46,60 +33,22 @@ class Account { * @param Team[] $teams * @param int $id */ - public function __construct(string $email, string $phoneNumber, AccountUser $user, array $teams, int $id) { + public function __construct(string $email, string $name, string $token) { $this->email = $email; - $this->phoneNumber = $phoneNumber; - $this->user = $user; - $this->teams = $teams; - $this->id = $id; + $this->name = $name; + $this->token = $token; } - /** - * @return string - */ public function getEmail(): string { return $this->email; } - /** - * @param string $email - */ - public function setEmail(string $email): void { - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - throw new InvalidArgumentException("Invalid mail address"); - } - $this->email = $email; - } - - /** - * @return string - */ - public function getPhoneNumber(): string { - return $this->phoneNumber; + public function getToken(): string { + return $this->token; } - /** - * @param string $phoneNumber - */ - public function setPhoneNumber(string $phoneNumber): void { - if (!preg_match(PHONE_NUMBER_REGEXP, $phoneNumber)) { - throw new InvalidArgumentException("Invalid phone number"); - } - $this->phoneNumber = $phoneNumber; + public function getName(): string { + return $this->name; } - public function getId(): int { - return $this->id; - } - - /** - * @return Team[] - */ - public function getTeams(): array { - return $this->teams; - } - - public function getUser(): AccountUser { - return $this->user; - } } diff --git a/src/Gateway/AccountGateway.php b/src/Gateway/AccountGateway.php new file mode 100644 index 0000000..a4686d6 --- /dev/null +++ b/src/Gateway/AccountGateway.php @@ -0,0 +1,56 @@ +con = $con; + } + + + public function exists(string $email): bool { + return $this->getAccount($email) != null; + } + + + public function insertAccount(Account $account, string $hash): void { + $this->con->exec("INSERT INTO Account VALUES (:username,:hash,:email)", [ + ':username' => [$account->getName(), PDO::PARAM_STR], + ':hash' => [$hash, PDO::PARAM_STR], + ':email' => [$account->getEmail(), PDO::PARAM_STR], + ':token' => [$account->getToken(), PDO::PARAM_STR] + ]); + } + + public function getHash(string $email): string { + $results = $this->con->fetch("SELECT hash FROM Account WHERE email = :email", [ + ':email' => [$email, PDO::PARAM_STR] + ]); + return $results[0]['hash']; + } + + + /** + * @param string $email + * @return Account|null + */ + public function getAccount(string $email): ?Account { + $results = $this->con->fetch("SELECT username,email,token FROM Account WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]); + if (empty($results)) + return null; + + $acc = $results[0]; + return new Account($acc["email"], $acc["name"], $acc["token"]); + } + + +} diff --git a/src/Gateway/AuthGateway.php b/src/Gateway/AuthGateway.php deleted file mode 100644 index 5acc01c..0000000 --- a/src/Gateway/AuthGateway.php +++ /dev/null @@ -1,47 +0,0 @@ -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 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|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; - } - - - - -} diff --git a/src/Model/AuthModel.php b/src/Model/AuthModel.php index 312cca8..b786c2e 100644 --- a/src/Model/AuthModel.php +++ b/src/Model/AuthModel.php @@ -2,16 +2,19 @@ namespace App\Model; -use App\Gateway\AuthGateway; +use App\Controller\AuthController; +use App\Data\Account; +use App\Gateway\AccountGateway; use App\Validation\FieldValidationFail; use App\Validation\ValidationFail; class AuthModel { - private AuthGateway $gateway; + private AccountGateway $gateway; + /** - * @param AuthGateway $gateway + * @param AccountGateway $gateway */ - public function __construct(AuthGateway $gateway) { + public function __construct(AccountGateway $gateway) { $this->gateway = $gateway; } @@ -21,59 +24,54 @@ class AuthModel { * @param string $password * @param string $confirmPassword * @param string $email - * @return ValidationFail[] + * @param ValidationFail[] $failures + * @return Account|null the registered account or null if failures occurred */ - public function register(string $username, string $password, string $confirmPassword, string $email): array { - $errors = []; + public function register(string $username, string $password, string $confirmPassword, string $email, array &$failures): ?Account { if ($password != $confirmPassword) { - $errors[] = new FieldValidationFail("confirmpassword", "password and password confirmation are not equals"); + $failures[] = new FieldValidationFail("confirmpassword", "password and password confirmation are not equals"); } - if ($this->gateway->mailExist($email)) { - $errors[] = new FieldValidationFail("email", "email already exist"); + if ($this->gateway->exists($email)) { + $failures[] = new FieldValidationFail("email", "email already exist"); } - if(empty($errors)) { - $hash = password_hash($password, PASSWORD_DEFAULT); - $this->gateway->insertAccount($username, $hash, $email); + if (!empty($errors)) { + return null; } - return $errors; - } + $hash = password_hash($password, PASSWORD_DEFAULT); - /** - * @param string $email - * @return array|null - */ - public function getUserFields(string $email): ?array { - return $this->gateway->getUserFields($email); + $account = new Account($email, $username, $this->generateToken()); + $this->gateway->insertAccount($account, $hash); + return $account; } + private function generateToken(): string { + return base64_encode(random_bytes(64)); + } /** * @param string $email * @param string $password - * @return ValidationFail[] $errors + * @param ValidationFail[] $failures + * @return Account|null the authenticated account or null if failures occurred */ - public function login(string $email, string $password): array { - $errors = []; - - if (!$this->gateway->mailExist($email)) { - $errors[] = new FieldValidationFail("email", "email doesnt exists"); - return $errors; + public function login(string $email, string $password, array &$failures): ?Account { + if (!$this->gateway->exists($email)) { + $failures = new FieldValidationFail("email", "email doesnt exists"); + return null; } - $hash = $this->gateway->getUserHash($email); + $hash = $this->gateway->getHash($email); if (!password_verify($password, $hash)) { - $errors[] = new FieldValidationFail("password", "invalid password"); + $failures = new FieldValidationFail("password", "invalid password"); + return null; } - return $errors; + return $this->gateway->getAccount($email); } - - - } diff --git a/src/Session/MutableSessionHandle.php b/src/Session/MutableSessionHandle.php new file mode 100644 index 0000000..a7822a3 --- /dev/null +++ b/src/Session/MutableSessionHandle.php @@ -0,0 +1,9 @@ + Date: Tue, 21 Nov 2023 21:30:12 +0100 Subject: [PATCH 31/75] front-controller (#17) I did the FRONT CONTROLLERRRRR. (Salva version lol) # When you create a new controller When you create a new controller, define the path to call it here (image 1). Don't forget to associate a controller with a role. # The action's main method The main method have to return a `ViewHttpResponse`. If you create a new action, please make sure that the action's main method have the same name than the action (image 2 and 3) and to make the main method public. Co-authored-by: DahmaneYanis Co-authored-by: d_yanis Co-authored-by: Override-6 Reviewed-on: https://codefirst.iut.uca.fr/git/IQBall/Application-Web/pulls/17 Co-authored-by: Yanis DAHMANE-BOUNOUA Co-committed-by: Yanis DAHMANE-BOUNOUA --- Documentation/http.puml | 2 +- Documentation/php.puml | 8 ++ public/index.php | 81 +------------- src/Controller/EditorController.php | 14 ++- src/Controller/FrontController.php | 162 ++++++++++++++++++++++++++++ src/Controller/UserController.php | 16 +++ src/Views/home.twig | 13 +++ 7 files changed, 210 insertions(+), 86 deletions(-) create mode 100644 Documentation/php.puml create mode 100644 src/Controller/FrontController.php create mode 100644 src/Controller/UserController.php create mode 100644 src/Views/home.twig diff --git a/Documentation/http.puml b/Documentation/http.puml index b41135d..9fbe606 100644 --- a/Documentation/http.puml +++ b/Documentation/http.puml @@ -35,7 +35,7 @@ class ViewHttpResponse extends HttpResponse { - arguments: array - kind: int - + __construct(kind: int, file: string, arguments: array, code: int = HttpCodes::OK) + - __construct(kind: int, file: string, arguments: array, code: int = HttpCodes::OK) + getViewKind(): int + getFile(): string + getArguments(): array diff --git a/Documentation/php.puml b/Documentation/php.puml new file mode 100644 index 0000000..8641900 --- /dev/null +++ b/Documentation/php.puml @@ -0,0 +1,8 @@ +@startuml + +class FrontController { + - router : AltoRouter + +} + +@enduml \ No newline at end of file diff --git a/public/index.php b/public/index.php index edc4cb3..f898d7f 100644 --- a/public/index.php +++ b/public/index.php @@ -4,83 +4,10 @@ require "../vendor/autoload.php"; require "../config.php"; require "../sql/database.php"; require "utils.php"; +require "../src/react-display.php"; -use App\Connexion; -use App\Controller\EditorController; -use App\Controller\SampleFormController; -use App\Gateway\FormResultGateway; -use App\Gateway\TacticInfoGateway; -use App\Http\JsonHttpResponse; -use App\Http\ViewHttpResponse; -use App\Model\TacticModel; -use Twig\Loader\FilesystemLoader; -use App\Gateway\AuthGateway; -use App\Controller\AuthController; -use App\Validation\ValidationFail; -use App\Controller\ErrorController; -use App\Controller\VisualizerController; - -$loader = new FilesystemLoader('../src/Views/'); -$twig = new \Twig\Environment($loader); +use App\Controller\FrontController; $basePath = get_public_path(); -$con = new Connexion(get_database()); - -// routes initialization -$router = new AltoRouter(); -$router->setBasePath($basePath); - -$sampleFormController = new SampleFormController(new FormResultGateway($con)); -$authGateway = new AuthGateway($con); -$authController = new \App\Controller\AuthController(new \App\Model\AuthModel($authGateway)); -$editorController = new EditorController(new TacticModel(new TacticInfoGateway($con))); -$visualizerController = new VisualizerController(new TacticModel(new TacticInfoGateway($con))); - - -$router->map("GET", "/", fn() => $sampleFormController->displayFormReact()); -$router->map("POST", "/submit", fn() => $sampleFormController->submitFormReact($_POST)); -$router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); -$router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); -$router->map("GET", "/register", fn() => $authController->displayRegister()); -$router->map("POST", "/register", fn() => $authController->confirmRegister($_POST)); -$router->map("GET", "/login", fn() => $authController->displayLogin()); -$router->map("POST", "/login", fn() => $authController->confirmLogin($_POST)); -$router->map("GET", "/tactic/new", fn() => $editorController->makeNew()); -$router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->openEditorFor($id)); -$router->map("GET", "/tactic/[i:id]", fn(int $id) => $visualizerController->openVisualizer($id)); - -$match = $router->match(); - -if ($match == null) { - http_response_code(404); - ErrorController::displayFailures([ValidationFail::notFound("Cette page n'existe pas")], $twig); - return; -} - -$response = call_user_func_array($match['target'], $match['params']); - -http_response_code($response->getCode()); - -if ($response instanceof ViewHttpResponse) { - $file = $response->getFile(); - $args = $response->getArguments(); - - switch ($response->getViewKind()) { - case ViewHttpResponse::REACT_VIEW: - send_react_front($file, $args); - break; - case ViewHttpResponse::TWIG_VIEW: - try { - $twig->display($file, $args); - } catch (\Twig\Error\RuntimeError|\Twig\Error\SyntaxError $e) { - http_response_code(500); - echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); - throw $e; - } - break; - } - -} elseif ($response instanceof JsonHttpResponse) { - header('Content-type: application/json'); - echo $response->getJson(); -} +$frontController = new FrontController($basePath); +$frontController->run(); diff --git a/src/Controller/EditorController.php b/src/Controller/EditorController.php index ed270d1..eb07184 100644 --- a/src/Controller/EditorController.php +++ b/src/Controller/EditorController.php @@ -2,9 +2,10 @@ namespace App\Controller; +use App\Connexion; use App\Data\TacticInfo; +use App\Gateway\TacticInfoGateway; use App\Http\HttpCodes; -use App\Http\HttpRequest; use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; @@ -13,18 +14,15 @@ use App\Model\TacticModel; class EditorController { private TacticModel $model; - /** - * @param TacticModel $model - */ - public function __construct(TacticModel $model) { - $this->model = $model; + public function __construct() { + $this->model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); } private function openEditor(TacticInfo $tactic): HttpResponse { return ViewHttpResponse::react("views/Editor.tsx", ["name" => $tactic->getName(), "id" => $tactic->getId()]); } - public function makeNew(): HttpResponse { + public function create(): HttpResponse { $tactic = $this->model->makeNewDefault(); return $this->openEditor($tactic); } @@ -34,7 +32,7 @@ class EditorController { * @param int $id the targeted tactic identifier * @return HttpResponse */ - public function openEditorFor(int $id): HttpResponse { + public function edit(int $id): HttpResponse { $tactic = $this->model->get($id); if ($tactic == null) { diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php new file mode 100644 index 0000000..66d5d4f --- /dev/null +++ b/src/Controller/FrontController.php @@ -0,0 +1,162 @@ +router = $this->createRouter($basePath); + $this->initializeRouterMap(); + } + + /** + * Main behavior of the FrontController + * + * @return void + */ + public function run(): void { + $match = $this->router->match(); + if ($match != null) { + $this->handleMatch($match); + } else { + $this->displayViewByKind(ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND)); + } + } + + /** + * Create a new instance of an AltoRouter + * + * @param string $basePath + * @return AltoRouter + */ + public function createRouter(string $basePath): AltoRouter { + $router = new AltoRouter(); + $router->setBasePath($basePath); + return $router; + } + + /** + * Initialize project's routes + * + * @return void + */ + private function initializeRouterMap(): void { + $this->router->map("GET", "/", "UserController"); + $this->router->map("GET", "/[a:action]?", "UserController"); + $this->router->map("GET", "/tactic/[a:action]/[i:idTactic]?", "EditorController"); + } + + /** + * @param array $match + * @return void + */ + private function handleMatch(array $match): void { + $tag = $match['target']; + + $action = $this->getAction($match); + $params = $match["params"]; + unset($params['action']); + $this->handleResponseByType($this->tryToCall($tag, $action, array_values($params))); + } + + /** + * @param string $controller + * @param string $action + * @param array $params + * @return HttpResponse + */ + private function tryToCall(string $controller, string $action, array $params): HttpResponse { + $controller = $this->getController($controller); + try { + if (is_callable([$controller, $action])) { + return call_user_func_array([$controller, $action], $params); + } else { + return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); + } + } catch (Exception $e) { + return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); + } + } + + /** + * Get the right method to call to do an action + * + * @param array $match + * @return string + */ + private function getAction(array $match): string { + if (isset($match["params"]["action"])) { + return $match["params"]["action"]; + } + return "default"; + } + + /** + * Initialize the right controller by the user's role + * + * @param string $controller + * @return mixed + */ + private function getController(string $controller) { + $namespace = "\\App\\Controller\\"; + $controller = $namespace . $controller; + return new $controller(); + } + + /** + * Redirect the return by the response's type + * + * @param HttpResponse $response + * @return void + */ + private function handleResponseByType(HttpResponse $response): void { + http_response_code($response->getCode()); + if ($response instanceof ViewHttpResponse) { + $this->displayViewByKind($response); + } elseif ($response instanceof JsonHttpResponse) { + header('Content-type: application/json'); + echo $response->getJson(); + } + } + + /** + * Use the right method to display the response + * + * @param ViewHttpResponse $response + * @return void + */ + private function displayViewByKind(ViewHttpResponse $response): void { + $file = $response->getFile(); + $args = $response->getArguments(); + + switch ($response->getViewKind()) { + case ViewHttpResponse::REACT_VIEW: + send_react_front($file, $args); + break; + case ViewHttpResponse::TWIG_VIEW: + try { + $loader = new FilesystemLoader('../src/Views/'); + $twig = new Environment($loader); + $twig->display($file, $args); + } catch (RuntimeError|SyntaxError|LoaderError $e) { + http_response_code(500); + echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); + throw $e; + } + break; + } + } +} diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php new file mode 100644 index 0000000..930cd67 --- /dev/null +++ b/src/Controller/UserController.php @@ -0,0 +1,16 @@ + + + + + + + Document + + +

Page Home à faire

+ + \ No newline at end of file From 27c7c89b0b71f1151c8a125fdb534a208a7792e4 Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Tue, 21 Nov 2023 22:43:33 +0100 Subject: [PATCH 32/75] WIP HomePage --- src/Views/home.twig | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Views/home.twig b/src/Views/home.twig index b65af07..600ed1d 100644 --- a/src/Views/home.twig +++ b/src/Views/home.twig @@ -9,5 +9,31 @@

Page Home à faire

+ +

Mes équipes

+ +{% for team in recentTeam %} +
+

{{team.name}}

+
+{% endfor %} + +

Mes strategies

+ + + +{% for tactic in recentTactic %} +
+

{{tactic.id}} - {{tactic.name}} - {{tactic.creation_date}}

+
+{% endfor %} + +

+ + + + + + \ No newline at end of file From 1b9a873cdce83db0b6f91e246ae72b7b7035d09a Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Tue, 21 Nov 2023 23:24:20 +0100 Subject: [PATCH 33/75] Updated documentation + started to add some documentation --- Documentation/team.puml | 24 ++++----- src/Controller/TeamController.php | 4 +- src/Views/display_team.html.twig | 84 ++++++++++++++++++++----------- 3 files changed, 69 insertions(+), 43 deletions(-) diff --git a/Documentation/team.puml b/Documentation/team.puml index b8374cd..a291b1d 100644 --- a/Documentation/team.puml +++ b/Documentation/team.puml @@ -12,12 +12,14 @@ class Team { } Team --> "- mainColor" Color -Team --> "- secondaryColor" Color +Team --> "- secondColor" Color class Color { - - value: int - - + getValue(): int + - value: string + - __construct(value : string) + + getValue(): string + + from(value: string): Color + + tryFrom(value : string) : ?Color } class TeamGateway{ @@ -31,15 +33,11 @@ TeamGateway *--"- con" Connexion TeamGateway ..> Color class TeamModel{ - + ERROR_INVALID_COLOR : int {readOnly} - + ERROR_INVALID_NAME : int {readOnly} - + ERROR_INVALID_PICTURE : int {readOnly} - + ERROR_INVALID_SEARCH : int {readOnly} - + ERROR_NO_DATA_FOUND : int {readOnly} --- + __construct(gateway : TeamGateway) + createTeam(name : string,picture : string, mainColorValue : int, secondColorValue : int, errors : array) + listByName(name : string ,errors : array) : ?array + + displayTeam(id : int): Team } TeamModel *--"- gateway" TeamGateway @@ -50,8 +48,11 @@ class TeamController{ - twig : Environement -- + __construct( model : TeamModel, twig : Environement) - + submitTeam(request : array) - + listTeamByName(request : array) + + displaySubmitTeam() : HttpResponse + + submitTeam(request : array) : HttpResponse + + displayListTeamByName(): HttpResponse + + listTeamByName(request : array) : HttpResponse + + displayTeam(id : int): HttpResponse } TeamController *--"- model" TeamModel @@ -62,7 +63,6 @@ class Connexion{ + __constructor(pdo : PDO) + exec(query : string, args : array) + fetch(query string, args array): array - } @enduml \ No newline at end of file diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index fa418c5..288e733 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -10,7 +10,7 @@ use App\Validation\FieldValidationFail; use App\Validation\Validators; use \Twig\Environment; -class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ +class TeamController { private TeamModel $model; private Environment $twig; @@ -24,7 +24,7 @@ class TeamController /* verif si les camp sont assignés, sinon erreur 400*/ $this->twig = $twig; } - public function displaySubmitTeam() { + public function displaySubmitTeam() : HttpResponse { return ViewHttpResponse::twig("insert_team.html.twig", []); } diff --git a/src/Views/display_team.html.twig b/src/Views/display_team.html.twig index e3b0fe0..f7bb02b 100644 --- a/src/Views/display_team.html.twig +++ b/src/Views/display_team.html.twig @@ -1,32 +1,58 @@ - - - Twig view - - - - -

{{ team.name }}

-Logo d'équipe -
-
- -{% for m in team.members %} -

m.id

-{% endfor %} - - + + + Twig view + + + +
+

IQBall

+
+ +
+ +
+

{{ team.name }}

+ Logo d'équipe +
+
+ + {% for m in team.members %} +

m.id

+ {% endfor %} +
+ +
+ \ No newline at end of file From 982acf5e09bba4aa8ae91965c55ce1fa534ee7ea Mon Sep 17 00:00:00 2001 From: Override-6 Date: Wed, 22 Nov 2023 01:58:50 +0100 Subject: [PATCH 34/75] integrate sessions for tactics --- sql/.guard | 0 sql/setup-tables.sql | 22 ++++--- src/Controller/FrontController.php | 66 ++++++++++++++------- src/Controller/Sub/AuthController.php | 11 +++- src/Controller/Sub/EditorController.php | 18 ++++-- src/Controller/Sub/VisualizerController.php | 11 +++- src/Controller/UserController.php | 33 +++-------- src/Controller/VisitorController.php | 30 ++++++++++ src/Data/Account.php | 17 ++++-- src/Data/TacticInfo.php | 13 +++- src/Gateway/AccountGateway.php | 15 ++--- src/Gateway/FormResultGateway.php | 35 ----------- src/Gateway/TacticInfoGateway.php | 15 +++-- src/Http/HttpCodes.php | 4 ++ src/Http/HttpResponse.php | 25 +++++++- src/Http/JsonHttpResponse.php | 2 +- src/Http/ViewHttpResponse.php | 2 +- src/Model/AuthModel.php | 13 ++-- src/Model/TacticModel.php | 8 +-- src/Session/MutableSessionHandle.php | 1 + src/Validation/ValidationFail.php | 2 +- src/Validator/TacticValidator.php | 22 +++++++ src/Views/error.html.twig | 2 +- 23 files changed, 231 insertions(+), 136 deletions(-) delete mode 100644 sql/.guard create mode 100644 src/Controller/VisitorController.php delete mode 100644 src/Gateway/FormResultGateway.php create mode 100644 src/Validator/TacticValidator.php diff --git a/sql/.guard b/sql/.guard deleted file mode 100644 index e69de29..0000000 diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index a289336..457803c 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -5,20 +5,24 @@ DROP TABLE IF EXISTS TacticInfo; CREATE TABLE FormEntries ( - name varchar, - description varchar + name varchar NOT NULL, + description varchar NOT NULL ); CREATE TABLE Account ( - username varchar, - hash varchar, - email varchar unique, - token varchar(256) NOT NULL UNIQUE + id integer PRIMARY KEY AUTOINCREMENT, + email varchar UNIQUE NOT NULL, + username varchar NOT NULL, + token varchar UNIQUE NOT NULL, + hash varchar NOT NULL ); CREATE TABLE TacticInfo ( id integer PRIMARY KEY AUTOINCREMENT, - name varchar, - creation_date timestamp DEFAULT CURRENT_TIMESTAMP -); \ No newline at end of file + name varchar NOT NULL, + creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + owner integer NOT NULL, + FOREIGN KEY (owner) REFERENCES Account +); + diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index d5c6417..3d73d80 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -8,7 +8,7 @@ use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; use App\Session\MutableSessionHandle; -use Exception; +use App\Validation\ValidationFail; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; @@ -17,23 +17,33 @@ use Twig\Loader\FilesystemLoader; class FrontController { private AltoRouter $router; + private string $basePath; + + private const USER_CONTROLLER = "UserController"; + private const VISITOR_CONTROLLER = "VisitorController"; + public function __construct(string $basePath) { $this->router = $this->createRouter($basePath); $this->initializeRouterMap(); + $this->basePath = $basePath; } /** - * Main behavior of the FrontController - * + * @param MutableSessionHandle $session * @return void + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError */ public function run(MutableSessionHandle $session): void { $match = $this->router->match(); - if ($match != null) { + if ($match) { $this->handleMatch($match, $session); } else { - $this->displayViewByKind(ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND)); + $this->displayViewByKind(ViewHttpResponse::twig("error.html.twig", [ + 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")] + ], HttpCodes::NOT_FOUND)); } } @@ -55,13 +65,14 @@ class FrontController { * @return void */ private function initializeRouterMap(): void { - $this->router->map("GET", "/", "UserController"); - $this->router->map("GET|POST", "/[a:action]?", "UserController"); - $this->router->map("GET|POST", "/tactic/[a:action]/[i:idTactic]?", "UserController"); + $this->router->map("GET", "/home", self::USER_CONTROLLER); + $this->router->map("GET|POST", "/user/[a:action]/[i:idTactic]?", self::USER_CONTROLLER); + $this->router->map("GET|POST", "/visitor/[a:action]", self::VISITOR_CONTROLLER); } /** * @param array $match + * @param MutableSessionHandle $session * @return void */ private function handleMatch(array $match, MutableSessionHandle $session): void { @@ -73,26 +84,32 @@ class FrontController { $this->handleResponseByType($this->tryToCall($tag, $action, array_values($params), $session)); } + /** - * @param string $controller + * @param string $controllerName * @param string $action * @param array $params * @param MutableSessionHandle $session * @return HttpResponse */ - - private function tryToCall(string $controller, string $action, array $params, MutableSessionHandle $session): HttpResponse { - $controller = $this->getController($controller); - try { - if (is_callable([$controller, $action])) { - // append the session as the last parameter of a controller function - $params[] = $session; - return call_user_func_array([$controller, $action], $params); - } else { - return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); + private function tryToCall(string $controllerName, string $action, array $params, MutableSessionHandle $session): HttpResponse { + if ($controllerName != self::VISITOR_CONTROLLER) { + $account = $session->getAccount(); + if ($account == null) { + return HttpResponse::redirect($this->basePath . "/visitor/login"); } - } catch (Exception $e) { - return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); + } + + $controller = $this->getController($controllerName); + + if (is_callable([$controller, $action])) { + // append the session as the last parameter of a controller function + $params[] = $session; + return call_user_func_array([$controller, $action], $params); + } else { + return ViewHttpResponse::twig("error.html.twig", [ + 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")] + ], HttpCodes::NOT_FOUND); } } @@ -106,7 +123,7 @@ class FrontController { if (isset($match["params"]["action"])) { return $match["params"]["action"]; } - return "default"; + return "home"; } /** @@ -129,6 +146,11 @@ class FrontController { */ private function handleResponseByType(HttpResponse $response): void { http_response_code($response->getCode()); + + foreach ($response->getHeaders() as $header => $value) { + header("$header: $value"); + } + if ($response instanceof ViewHttpResponse) { $this->displayViewByKind($response); } elseif ($response instanceof JsonHttpResponse) { diff --git a/src/Controller/Sub/AuthController.php b/src/Controller/Sub/AuthController.php index 7a537f2..fc72f67 100644 --- a/src/Controller/Sub/AuthController.php +++ b/src/Controller/Sub/AuthController.php @@ -7,6 +7,7 @@ use App\Http\HttpRequest; use App\Http\HttpResponse; use App\Http\ViewHttpResponse; use App\Model\AuthModel; +use App\Session\MutableSessionHandle; use App\Validation\FieldValidationFail; use App\Validation\ValidationFail; use App\Validation\Validators; @@ -42,9 +43,10 @@ class AuthController { /** * @param mixed[] $request + * @param MutableSessionHandle $session * @return HttpResponse */ - public function confirmRegister(array $request): HttpResponse { + public function confirmRegister(array $request, MutableSessionHandle $session): HttpResponse { $fails = []; $request = HttpRequest::from($request, $fails, [ "username" => [Validators::name(), Validators::lenBetween(2, 32)], @@ -59,6 +61,9 @@ class AuthController { if (!empty($fails)) { return $this->displayBadFields("display_register.html.twig", $fails); } + + $session->setAccount($account); + return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $account->getName(), 'email' => $account->getEmail()]); } @@ -71,7 +76,7 @@ class AuthController { * @param mixed[] $request * @return HttpResponse */ - public function confirmLogin(array $request): HttpResponse { + public function confirmLogin(array $request, MutableSessionHandle $session): HttpResponse { $fails = []; $request = HttpRequest::from($request, $fails, [ "password" => [Validators::lenBetween(6, 256)], @@ -86,6 +91,8 @@ class AuthController { return $this->displayBadFields("display_login.html.twig", $fails); } + $session->setAccount($account); + return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $account->getName(), 'email' => $account->getEmail()]); } diff --git a/src/Controller/Sub/EditorController.php b/src/Controller/Sub/EditorController.php index ce7a0a3..1188bc9 100644 --- a/src/Controller/Sub/EditorController.php +++ b/src/Controller/Sub/EditorController.php @@ -3,6 +3,7 @@ namespace App\Controller\Sub; use App\Connexion; +use App\Controller\VisitorController; use App\Data\TacticInfo; use App\Gateway\TacticInfoGateway; use App\Http\HttpCodes; @@ -10,6 +11,9 @@ use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; use App\Model\TacticModel; +use App\Session\SessionHandle; +use App\Validation\ValidationFail; +use App\Validator\TacticValidator; class EditorController { private TacticModel $model; @@ -22,24 +26,28 @@ class EditorController { return ViewHttpResponse::react("views/Editor.tsx", ["name" => $tactic->getName(), "id" => $tactic->getId()]); } - public function createNew(): HttpResponse { - $tactic = $this->model->makeNewDefault(); + public function createNew(SessionHandle $session): HttpResponse { + $tactic = $this->model->makeNewDefault($session->getAccount()->getId()); return $this->openEditor($tactic); } /** * returns an editor view for a given tactic * @param int $id the targeted tactic identifier + * @param SessionHandle $session * @return HttpResponse */ - public function edit(int $id): HttpResponse { + public function edit(int $id, SessionHandle $session): HttpResponse { $tactic = $this->model->get($id); - if ($tactic == null) { - return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND); + $failure = TacticValidator::validateAccess($tactic, $session->getAccount()->getId()); + + if ($failure != null) { + return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); } return $this->openEditor($tactic); } + } diff --git a/src/Controller/Sub/VisualizerController.php b/src/Controller/Sub/VisualizerController.php index e3b5663..f70b18e 100644 --- a/src/Controller/Sub/VisualizerController.php +++ b/src/Controller/Sub/VisualizerController.php @@ -7,6 +7,9 @@ use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; use App\Model\TacticModel; +use App\Session\SessionHandle; +use App\Validation\ValidationFail; +use App\Validator\TacticValidator; class VisualizerController { private TacticModel $tacticModel; @@ -19,11 +22,13 @@ class VisualizerController { $this->tacticModel = $tacticModel; } - public function visualize(int $id): HttpResponse { + public function visualize(int $id, SessionHandle $session): HttpResponse { $tactic = $this->tacticModel->get($id); - if ($tactic == null) { - return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND); + $failure = TacticValidator::validateAccess($tactic, $session->getAccount()->getId()); + + if ($failure != null) { + return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); } return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]); diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 360c9a3..64a0caa 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -3,46 +3,29 @@ namespace App\Controller; use App\Connexion; -use App\Gateway\AccountGateway; use App\Gateway\TacticInfoGateway; use App\Http\HttpResponse; use App\Http\ViewHttpResponse; -use App\Model\AuthModel; use App\Model\TacticModel; +use App\Session\SessionHandle; -class UserController { +class UserController extends VisitorController { public function home(): HttpResponse { return ViewHttpResponse::twig("home.twig", []); } - public function register(): HttpResponse { - $model = new AuthModel(new AccountGateway(new Connexion(get_database()))); - if ($_SERVER['REQUEST_METHOD'] === 'GET') { - return (new Sub\AuthController($model))->displayRegister(); - } - return (new Sub\AuthController($model))->confirmRegister($_POST); - } - - public function login(): HttpResponse { - $model = new AuthModel(new AccountGateway(new Connexion(get_database()))); - if ($_SERVER['REQUEST_METHOD'] === 'GET') { - return (new Sub\AuthController($model))->displayLogin(); - } - return (new Sub\AuthController($model))->confirmLogin($_POST); - } - - public function open(int $id): HttpResponse { + public function view(int $id, SessionHandle $session): HttpResponse { $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); - return (new Sub\VisualizerController($model))->visualize($id); + return (new Sub\VisualizerController($model))->visualize($id, $session); } - public function edit(int $id): HttpResponse { + public function edit(int $id, SessionHandle $session): HttpResponse { $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); - return (new Sub\EditorController($model))->edit($id); + return (new Sub\EditorController($model))->edit($id, $session); } - public function create(): HttpResponse { + public function create(SessionHandle $session): HttpResponse { $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); - return (new Sub\EditorController($model))->createNew(); + return (new Sub\EditorController($model))->createNew($session); } } diff --git a/src/Controller/VisitorController.php b/src/Controller/VisitorController.php new file mode 100644 index 0000000..56e0ec2 --- /dev/null +++ b/src/Controller/VisitorController.php @@ -0,0 +1,30 @@ +displayRegister(); + } + return (new Sub\AuthController($model))->confirmRegister($_POST, $session); + } + + public final function login(MutableSessionHandle $session): HttpResponse { + $model = new AuthModel(new AccountGateway(new Connexion(get_database()))); + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + return (new Sub\AuthController($model))->displayLogin(); + } + return (new Sub\AuthController($model))->confirmLogin($_POST, $session); + } + +} \ No newline at end of file diff --git a/src/Data/Account.php b/src/Data/Account.php index 8b58e1c..0ed4339 100755 --- a/src/Data/Account.php +++ b/src/Data/Account.php @@ -25,18 +25,27 @@ class Account { */ private string $name; + /** + * @var int + */ + private int $id; + /** * @param string $email - * @param string $phoneNumber - * @param AccountUser $user - * @param Team[] $teams + * @param string $name + * @param string $token * @param int $id */ - public function __construct(string $email, string $name, string $token) { + public function __construct(string $email, string $name, string $token, int $id) { $this->email = $email; $this->name = $name; $this->token = $token; + $this->id = $id; + } + + public function getId(): int { + return $this->id; } public function getEmail(): string { diff --git a/src/Data/TacticInfo.php b/src/Data/TacticInfo.php index eef7bb3..a2a576e 100644 --- a/src/Data/TacticInfo.php +++ b/src/Data/TacticInfo.php @@ -7,14 +7,18 @@ class TacticInfo implements \JsonSerializable { private string $name; private int $creation_date; + private int $ownerId; + /** * @param int $id * @param string $name * @param int $creation_date + * @param int $ownerId */ - public function __construct(int $id, string $name, int $creation_date) { + public function __construct(int $id, string $name, int $creation_date, int $ownerId) { $this->id = $id; $this->name = $name; + $this->ownerId = $ownerId; $this->creation_date = $creation_date; } @@ -26,6 +30,13 @@ class TacticInfo implements \JsonSerializable { return $this->name; } + /** + * @return int + */ + public function getOwnerId(): int { + return $this->ownerId; + } + public function getCreationTimestamp(): int { return $this->creation_date; } diff --git a/src/Gateway/AccountGateway.php b/src/Gateway/AccountGateway.php index a4686d6..8320f33 100644 --- a/src/Gateway/AccountGateway.php +++ b/src/Gateway/AccountGateway.php @@ -22,13 +22,14 @@ class AccountGateway { } - public function insertAccount(Account $account, string $hash): void { - $this->con->exec("INSERT INTO Account VALUES (:username,:hash,:email)", [ - ':username' => [$account->getName(), PDO::PARAM_STR], + public function insertAccount(string $name, string $email, string $token, string $hash): int { + $this->con->exec("INSERT INTO Account(username, hash, email, token) VALUES (:username,:hash,:email,:token)", [ + ':username' => [$name, PDO::PARAM_STR], ':hash' => [$hash, PDO::PARAM_STR], - ':email' => [$account->getEmail(), PDO::PARAM_STR], - ':token' => [$account->getToken(), PDO::PARAM_STR] + ':email' => [$email, PDO::PARAM_STR], + ':token' => [$token, PDO::PARAM_STR] ]); + return intval($this->con->lastInsertId()); } public function getHash(string $email): string { @@ -44,12 +45,12 @@ class AccountGateway { * @return Account|null */ public function getAccount(string $email): ?Account { - $results = $this->con->fetch("SELECT username,email,token FROM Account WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]); + $results = $this->con->fetch("SELECT * FROM Account WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]); if (empty($results)) return null; $acc = $results[0]; - return new Account($acc["email"], $acc["name"], $acc["token"]); + return new Account($acc["email"], $acc["username"], $acc["token"], $acc["id"]); } diff --git a/src/Gateway/FormResultGateway.php b/src/Gateway/FormResultGateway.php deleted file mode 100644 index 36178ad..0000000 --- a/src/Gateway/FormResultGateway.php +++ /dev/null @@ -1,35 +0,0 @@ -con = $con; - } - - - public function insert(string $username, string $description): void { - $this->con->exec( - "INSERT INTO FormEntries VALUES (:name, :description)", - [ - ":name" => [$username, PDO::PARAM_STR], - "description" => [$description, PDO::PARAM_STR], - ] - ); - } - - /** - * @return array - */ - public function listResults(): array { - return $this->con->fetch("SELECT * FROM FormEntries", []); - } -} diff --git a/src/Gateway/TacticInfoGateway.php b/src/Gateway/TacticInfoGateway.php index 3441c9a..6aa2cca 100644 --- a/src/Gateway/TacticInfoGateway.php +++ b/src/Gateway/TacticInfoGateway.php @@ -28,19 +28,22 @@ class TacticInfoGateway { $row = $res[0]; - return new TacticInfo($id, $row["name"], strtotime($row["creation_date"])); + return new TacticInfo($id, $row["name"], strtotime($row["creation_date"]), $row["owner"]); } - public function insert(string $name): TacticInfo { + public function insert(string $name, int $owner): TacticInfo { $this->con->exec( - "INSERT INTO TacticInfo(name) VALUES(:name)", - [":name" => [$name, PDO::PARAM_STR]] + "INSERT INTO TacticInfo(name, owner) VALUES(:name, :owner)", + [ + ":name" => [$name, PDO::PARAM_STR], + ":owner" => [$owner, PDO::PARAM_INT] + ] ); $row = $this->con->fetch( - "SELECT id, creation_date FROM TacticInfo WHERE :id = id", + "SELECT id, creation_date, owner FROM TacticInfo WHERE :id = id", [':id' => [$this->con->lastInsertId(), PDO::PARAM_INT]] )[0]; - return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"])); + return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"]), $row["owner"]); } public function updateName(int $id, string $name): void { diff --git a/src/Http/HttpCodes.php b/src/Http/HttpCodes.php index f9d550c..c2b01df 100644 --- a/src/Http/HttpCodes.php +++ b/src/Http/HttpCodes.php @@ -7,7 +7,11 @@ namespace App\Http; */ class HttpCodes { public const OK = 200; + public const FOUND = 302; public const BAD_REQUEST = 400; + + public const FORBIDDEN = 403; + public const NOT_FOUND = 404; } diff --git a/src/Http/HttpResponse.php b/src/Http/HttpResponse.php index 5d8c3bf..d6d6a9a 100644 --- a/src/Http/HttpResponse.php +++ b/src/Http/HttpResponse.php @@ -3,21 +3,42 @@ namespace App\Http; class HttpResponse { + + /** + * @var array + */ + private array $headers; private int $code; /** * @param int $code + * @param array $headers */ - public function __construct(int $code) { + public function __construct(int $code, array $headers) { $this->code = $code; + $this->headers = $headers; } public function getCode(): int { return $this->code; } + /** + * @return array + */ + public function getHeaders(): array { + return $this->headers; + } + public static function fromCode(int $code): HttpResponse { - return new HttpResponse($code); + return new HttpResponse($code, []); + } + + public static function redirect(string $url, int $code = HttpCodes::FOUND): HttpResponse { + if ($code < 300 || $code >= 400) { + throw new \InvalidArgumentException("given code is not a redirection http code"); + } + return new HttpResponse($code, ["Location" => $url]); } } diff --git a/src/Http/JsonHttpResponse.php b/src/Http/JsonHttpResponse.php index bbd3d80..a01ffd9 100644 --- a/src/Http/JsonHttpResponse.php +++ b/src/Http/JsonHttpResponse.php @@ -12,7 +12,7 @@ class JsonHttpResponse extends HttpResponse { * @param mixed $payload */ public function __construct($payload, int $code = HttpCodes::OK) { - parent::__construct($code); + parent::__construct($code, []); $this->payload = $payload; } diff --git a/src/Http/ViewHttpResponse.php b/src/Http/ViewHttpResponse.php index 2e517d7..9db80c5 100644 --- a/src/Http/ViewHttpResponse.php +++ b/src/Http/ViewHttpResponse.php @@ -26,7 +26,7 @@ class ViewHttpResponse extends HttpResponse { * @param array $arguments */ private function __construct(int $kind, string $file, array $arguments, int $code = HttpCodes::OK) { - parent::__construct($code); + parent::__construct($code, []); $this->kind = $kind; $this->file = $file; $this->arguments = $arguments; diff --git a/src/Model/AuthModel.php b/src/Model/AuthModel.php index b786c2e..a12ce9e 100644 --- a/src/Model/AuthModel.php +++ b/src/Model/AuthModel.php @@ -2,7 +2,6 @@ namespace App\Model; -use App\Controller\AuthController; use App\Data\Account; use App\Gateway\AccountGateway; use App\Validation\FieldValidationFail; @@ -37,15 +36,15 @@ class AuthModel { $failures[] = new FieldValidationFail("email", "email already exist"); } - if (!empty($errors)) { + if (!empty($failures)) { return null; } $hash = password_hash($password, PASSWORD_DEFAULT); - $account = new Account($email, $username, $this->generateToken()); - $this->gateway->insertAccount($account, $hash); - return $account; + $token = $this->generateToken(); + $accountId = $this->gateway->insertAccount($username, $email, $token, $hash); + return new Account($email, $username, $token, $accountId); } private function generateToken(): string { @@ -60,13 +59,13 @@ class AuthModel { */ public function login(string $email, string $password, array &$failures): ?Account { if (!$this->gateway->exists($email)) { - $failures = new FieldValidationFail("email", "email doesnt exists"); + $failures[] = new FieldValidationFail("email", "email doesnt exists"); return null; } $hash = $this->gateway->getHash($email); if (!password_verify($password, $hash)) { - $failures = new FieldValidationFail("password", "invalid password"); + $failures[] = new FieldValidationFail("password", "invalid password"); return null; } diff --git a/src/Model/TacticModel.php b/src/Model/TacticModel.php index cabcdec..73cfd91 100644 --- a/src/Model/TacticModel.php +++ b/src/Model/TacticModel.php @@ -18,12 +18,12 @@ class TacticModel { $this->tactics = $tactics; } - public function makeNew(string $name): TacticInfo { - return $this->tactics->insert($name); + public function makeNew(string $name, int $ownerId): TacticInfo { + return $this->tactics->insert($name, $ownerId); } - public function makeNewDefault(): ?TacticInfo { - return $this->tactics->insert(self::TACTIC_DEFAULT_NAME); + public function makeNewDefault(int $ownerId): ?TacticInfo { + return $this->tactics->insert(self::TACTIC_DEFAULT_NAME, $ownerId); } /** diff --git a/src/Session/MutableSessionHandle.php b/src/Session/MutableSessionHandle.php index a7822a3..e142049 100644 --- a/src/Session/MutableSessionHandle.php +++ b/src/Session/MutableSessionHandle.php @@ -5,5 +5,6 @@ namespace App\Session; use App\Data\Account; interface MutableSessionHandle extends SessionHandle { + public function setAccount(Account $account): void; } \ No newline at end of file diff --git a/src/Validation/ValidationFail.php b/src/Validation/ValidationFail.php index 4f1ec22..83d6c76 100644 --- a/src/Validation/ValidationFail.php +++ b/src/Validation/ValidationFail.php @@ -34,7 +34,7 @@ class ValidationFail implements JsonSerializable { } public static function notFound(string $message): ValidationFail { - return new ValidationFail("not found", $message); + return new ValidationFail("Not found", $message); } } diff --git a/src/Validator/TacticValidator.php b/src/Validator/TacticValidator.php new file mode 100644 index 0000000..711fb72 --- /dev/null +++ b/src/Validator/TacticValidator.php @@ -0,0 +1,22 @@ +getId() . " n'existe pas"); + } + + if ($tactic->getOwnerId() != $ownerId) { + return new ValidationFail("Unauthorized", "Vous ne pouvez pas accéder à cette tactique.",); + } + + return null; + } + +} \ No newline at end of file diff --git a/src/Views/error.html.twig b/src/Views/error.html.twig index 1d1db7d..e30c2fa 100644 --- a/src/Views/error.html.twig +++ b/src/Views/error.html.twig @@ -51,7 +51,7 @@ {% endfor %} - + \ No newline at end of file From 39329c93257741db65a6f78cfe8b373dbf151523 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Wed, 22 Nov 2023 03:43:03 +0100 Subject: [PATCH 35/75] integrate session in API, use tokens --- phpstan.neon | 2 + public/api/index.php | 126 ++++++++++++++++++-- public/index.php | 4 +- src/Controller/Api/APIAuthController.php | 39 ++++++ src/Controller/Api/APITacticController.php | 22 ++-- src/Controller/FrontController.php | 4 +- src/Controller/Sub/AuthController.php | 1 - src/Controller/Sub/EditorController.php | 2 +- src/Controller/Sub/VisualizerController.php | 2 +- src/Controller/VisitorController.php | 8 +- src/Gateway/AccountGateway.php | 47 +++++--- src/Gateway/TacticInfoGateway.php | 2 +- src/Http/HttpResponse.php | 1 - src/Model/AuthModel.php | 2 +- src/Model/TacticModel.php | 19 ++- src/Session/MutableSessionHandle.php | 3 +- src/Session/PhpSessionHandle.php | 5 +- src/Session/SessionHandle.php | 4 +- src/Validation/ValidationFail.php | 4 + src/Validator/TacticValidator.php | 9 +- 20 files changed, 239 insertions(+), 67 deletions(-) create mode 100644 src/Controller/Api/APIAuthController.php diff --git a/phpstan.neon b/phpstan.neon index bc6c041..715fe84 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,5 +9,7 @@ parameters: - sql/database.php - profiles/dev-config-profile.php - profiles/prod-config-profile.php + - public/api/index.php excludePaths: - src/react-display-file.php + - public/api/index.php diff --git a/public/api/index.php b/public/api/index.php index 3ed5caa..8cc7287 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -6,32 +6,134 @@ require "../../sql/database.php"; require "../utils.php"; use App\Connexion; +use App\Controller\Api\APIAuthController; use App\Controller\Api\APITacticController; +use App\Data\Account; +use App\Gateway\AccountGateway; use App\Gateway\TacticInfoGateway; +use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; +use App\Model\AuthModel; use App\Model\TacticModel; +use App\Session\SessionHandle; +use App\Validation\ValidationFail; -$con = new Connexion(get_database()); +function getTacticController(): APITacticController { + return new APITacticController(new TacticModel(new TacticInfoGateway(new Connexion(get_database())))); +} + +function getAuthController(): APIAuthController { + return new APIAuthController(new AuthModel(new AccountGateway(new Connexion(get_database())))); +} + +class Action { + /** + * @var callable(mixed[]): HttpResponse $action action to call + */ + private $action; + + private bool $isAuthRequired; + + /** + * @param callable(mixed[]): HttpResponse $action + */ + private function __construct(callable $action, bool $isAuthRequired) { + $this->action = $action; + $this->isAuthRequired = $isAuthRequired; + } + + public function isAuthRequired(): bool { + return $this->isAuthRequired; + } + + /** + * @param mixed[] $params + * @param ?Account $account + * @return HttpResponse + */ + public function run(array $params, ?Account $account): HttpResponse { + $params = array_values($params); + if ($this->isAuthRequired) { + if ($account == null) { + throw new Exception("action requires authorization."); + } + $params[] = $account; + } + + return call_user_func_array($this->action, $params); + } + + /** + * @param callable(mixed[]): HttpResponse $action + * @return Action an action that does not require to have an authorization. + */ + public static function noAuth(callable $action): Action { + return new Action($action, false); + } + + /** + * @param callable(mixed[]): HttpResponse $action + * @return Action an action that does require to have an authorization. + */ + public static function auth(callable $action): Action { + return new Action($action, true); + } +} + +/** + * @param mixed[] $match + * @return HttpResponse + * @throws Exception + */ +function handleMatch(array $match): HttpResponse { + if (!$match) { + return new JsonHttpResponse([ValidationFail::notFound("not found")]); + } + + $action = $match['target']; + if (!$action instanceof Action) { + throw new Exception("routed action is not an Action object."); + } + + $auth = null; + + if ($action->isAuthRequired()) { + $auth = tryGetAuthAccount(); + if ($auth == null) { + return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header")]); + } + } + + return $action->run($match['params'], $auth); +} + +function tryGetAuthAccount(): ?Account { + $headers = getallheaders(); + + // If no authorization header is set, try fallback to php session. + if (!isset($headers['Authorization'])) { + $session = SessionHandle::init(); + return $session->getAccount(); + } + + $token = $headers['Authorization']; + $gateway = new AccountGateway(new Connexion(get_database())); + return $gateway->getAccountFromToken($token); +} $router = new AltoRouter(); $router->setBasePath(get_public_path() . "/api"); -$tacticEndpoint = new APITacticController(new TacticModel(new TacticInfoGateway($con))); -$router->map("POST", "/tactic/[i:id]/edit/name", fn(int $id) => $tacticEndpoint->updateName($id)); -$router->map("GET", "/tactic/[i:id]", fn(int $id) => $tacticEndpoint->getTacticInfo($id)); -$router->map("POST", "/tactic/new", fn() => $tacticEndpoint->newTactic()); +$router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); +$router->map("GET", "/tactic/[i:id]", Action::auth(fn(int $id, Account $acc) => getTacticController()->getTacticInfo($id, $acc))); +$router->map("POST", "/tactic/new", Action::auth(fn(Account $acc) => getTacticController()->newTactic($acc))); +$router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize())); $match = $router->match(); -if ($match == null) { - echo "404 not found"; - header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); - exit(1); -} - -$response = call_user_func_array($match['target'], $match['params']); +$response = handleMatch($match); http_response_code($response->getCode()); if ($response instanceof JsonHttpResponse) { diff --git a/public/index.php b/public/index.php index ce1df33..7d42305 100644 --- a/public/index.php +++ b/public/index.php @@ -8,9 +8,9 @@ require "utils.php"; require "../src/react-display.php"; use App\Controller\FrontController; -use App\Session\PhpSessionHandle; +use App\Session\SessionHandle; $basePath = get_public_path(); $frontController = new FrontController($basePath); -$frontController->run(PhpSessionHandle::init()); +$frontController->run(SessionHandle::init()); diff --git a/src/Controller/Api/APIAuthController.php b/src/Controller/Api/APIAuthController.php new file mode 100644 index 0000000..d9e4c23 --- /dev/null +++ b/src/Controller/Api/APIAuthController.php @@ -0,0 +1,39 @@ +model = $model; + } + + + public function authorize(): HttpResponse { + return Control::runChecked([ + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)], + "password" => [Validators::lenBetween(6, 256)], + ], function (HttpRequest $req) { + $failures = []; + $account = $this->model->login($req["email"], $req["password"], $failures); + + if (!empty($failures)) { + return new JsonHttpResponse($failures); + } + + return new JsonHttpResponse(["authorization" => $account->getToken()]); + }, true); + } + +} diff --git a/src/Controller/Api/APITacticController.php b/src/Controller/Api/APITacticController.php index ec0edc8..c1fcee3 100644 --- a/src/Controller/Api/APITacticController.php +++ b/src/Controller/Api/APITacticController.php @@ -3,6 +3,7 @@ namespace App\Controller\Api; use App\Controller\Control; +use App\Data\Account; use App\Http\HttpCodes; use App\Http\HttpRequest; use App\Http\HttpResponse; @@ -23,26 +24,33 @@ class APITacticController { $this->model = $model; } - public function updateName(int $tactic_id): HttpResponse { + public function updateName(int $tactic_id, Account $account): HttpResponse { return Control::runChecked([ "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], - ], function (HttpRequest $request) use ($tactic_id) { - $this->model->updateName($tactic_id, $request["name"]); + ], function (HttpRequest $request) use ($tactic_id, $account) { + + $failures = $this->model->updateName($tactic_id, $request["name"], $account->getId()); + + if (!empty($failures)) { + //TODO find a system to handle Unauthorized error codes more easily from failures. + return new JsonHttpResponse($failures, HttpCodes::BAD_REQUEST); + } + return HttpResponse::fromCode(HttpCodes::OK); }, true); } - public function newTactic(): HttpResponse { + public function newTactic(Account $account): HttpResponse { return Control::runChecked([ "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], - ], function (HttpRequest $request) { - $tactic = $this->model->makeNew($request["name"]); + ], function (HttpRequest $request) use ($account) { + $tactic = $this->model->makeNew($request["name"], $account->getId()); $id = $tactic->getId(); return new JsonHttpResponse(["id" => $id]); }, true); } - public function getTacticInfo(int $id): HttpResponse { + public function getTacticInfo(int $id, Account $account): HttpResponse { $tactic_info = $this->model->get($id); if ($tactic_info == null) { diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index 3d73d80..d1bbd6d 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -42,7 +42,7 @@ class FrontController { $this->handleMatch($match, $session); } else { $this->displayViewByKind(ViewHttpResponse::twig("error.html.twig", [ - 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")] + 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")], ], HttpCodes::NOT_FOUND)); } } @@ -108,7 +108,7 @@ class FrontController { return call_user_func_array([$controller, $action], $params); } else { return ViewHttpResponse::twig("error.html.twig", [ - 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")] + 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")], ], HttpCodes::NOT_FOUND); } } diff --git a/src/Controller/Sub/AuthController.php b/src/Controller/Sub/AuthController.php index fc72f67..fcf12d5 100644 --- a/src/Controller/Sub/AuthController.php +++ b/src/Controller/Sub/AuthController.php @@ -2,7 +2,6 @@ namespace App\Controller\Sub; -use App\Gateway\AccountGateway; use App\Http\HttpRequest; use App\Http\HttpResponse; use App\Http\ViewHttpResponse; diff --git a/src/Controller/Sub/EditorController.php b/src/Controller/Sub/EditorController.php index 1188bc9..dff6e8a 100644 --- a/src/Controller/Sub/EditorController.php +++ b/src/Controller/Sub/EditorController.php @@ -40,7 +40,7 @@ class EditorController { public function edit(int $id, SessionHandle $session): HttpResponse { $tactic = $this->model->get($id); - $failure = TacticValidator::validateAccess($tactic, $session->getAccount()->getId()); + $failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId()); if ($failure != null) { return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); diff --git a/src/Controller/Sub/VisualizerController.php b/src/Controller/Sub/VisualizerController.php index f70b18e..c7e5098 100644 --- a/src/Controller/Sub/VisualizerController.php +++ b/src/Controller/Sub/VisualizerController.php @@ -25,7 +25,7 @@ class VisualizerController { public function visualize(int $id, SessionHandle $session): HttpResponse { $tactic = $this->tacticModel->get($id); - $failure = TacticValidator::validateAccess($tactic, $session->getAccount()->getId()); + $failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId()); if ($failure != null) { return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); diff --git a/src/Controller/VisitorController.php b/src/Controller/VisitorController.php index 56e0ec2..e74934a 100644 --- a/src/Controller/VisitorController.php +++ b/src/Controller/VisitorController.php @@ -9,9 +9,7 @@ use App\Model\AuthModel; use App\Session\MutableSessionHandle; class VisitorController { - - - public final function register(MutableSessionHandle $session): HttpResponse { + final public function register(MutableSessionHandle $session): HttpResponse { $model = new AuthModel(new AccountGateway(new Connexion(get_database()))); if ($_SERVER['REQUEST_METHOD'] === 'GET') { return (new Sub\AuthController($model))->displayRegister(); @@ -19,7 +17,7 @@ class VisitorController { return (new Sub\AuthController($model))->confirmRegister($_POST, $session); } - public final function login(MutableSessionHandle $session): HttpResponse { + final public function login(MutableSessionHandle $session): HttpResponse { $model = new AuthModel(new AccountGateway(new Connexion(get_database()))); if ($_SERVER['REQUEST_METHOD'] === 'GET') { return (new Sub\AuthController($model))->displayLogin(); @@ -27,4 +25,4 @@ class VisitorController { return (new Sub\AuthController($model))->confirmLogin($_POST, $session); } -} \ No newline at end of file +} diff --git a/src/Gateway/AccountGateway.php b/src/Gateway/AccountGateway.php index 8320f33..3a57abb 100644 --- a/src/Gateway/AccountGateway.php +++ b/src/Gateway/AccountGateway.php @@ -17,39 +17,56 @@ class AccountGateway { } - public function exists(string $email): bool { - return $this->getAccount($email) != null; - } - - public function insertAccount(string $name, string $email, string $token, string $hash): int { $this->con->exec("INSERT INTO Account(username, hash, email, token) VALUES (:username,:hash,:email,:token)", [ ':username' => [$name, PDO::PARAM_STR], ':hash' => [$hash, PDO::PARAM_STR], ':email' => [$email, PDO::PARAM_STR], - ':token' => [$token, PDO::PARAM_STR] + ':token' => [$token, PDO::PARAM_STR], ]); return intval($this->con->lastInsertId()); } - public function getHash(string $email): string { - $results = $this->con->fetch("SELECT hash FROM Account WHERE email = :email", [ - ':email' => [$email, PDO::PARAM_STR] - ]); - return $results[0]['hash']; + /** + * @param string $email + * @return array|null + */ + private function getRowsFromMail(string $email): ?array { + return $this->con->fetch("SELECT * FROM Account WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]])[0] ?? null; + } + + + public function getHash(string $email): ?string { + $results = $this->getRowsFromMail($email); + if ($results == null) { + return null; + } + return $results['hash']; } + public function exists(string $email): bool { + return $this->getRowsFromMail($email) != null; + } /** * @param string $email * @return Account|null */ - public function getAccount(string $email): ?Account { - $results = $this->con->fetch("SELECT * FROM Account WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]); - if (empty($results)) + public function getAccountFromMail(string $email): ?Account { + $acc = $this->getRowsFromMail($email); + if (empty($acc)) { + return null; + } + + return new Account($email, $acc["username"], $acc["token"], $acc["id"]); + } + + public function getAccountFromToken(string $token): ?Account { + $acc = $this->con->fetch("SELECT * FROM Account WHERE token = :token", [':token' => [$token, PDO::PARAM_STR]])[0] ?? null; + if (empty($acc)) { return null; + } - $acc = $results[0]; return new Account($acc["email"], $acc["username"], $acc["token"], $acc["id"]); } diff --git a/src/Gateway/TacticInfoGateway.php b/src/Gateway/TacticInfoGateway.php index 6aa2cca..efa0a7c 100644 --- a/src/Gateway/TacticInfoGateway.php +++ b/src/Gateway/TacticInfoGateway.php @@ -36,7 +36,7 @@ class TacticInfoGateway { "INSERT INTO TacticInfo(name, owner) VALUES(:name, :owner)", [ ":name" => [$name, PDO::PARAM_STR], - ":owner" => [$owner, PDO::PARAM_INT] + ":owner" => [$owner, PDO::PARAM_INT], ] ); $row = $this->con->fetch( diff --git a/src/Http/HttpResponse.php b/src/Http/HttpResponse.php index d6d6a9a..e5db3e8 100644 --- a/src/Http/HttpResponse.php +++ b/src/Http/HttpResponse.php @@ -3,7 +3,6 @@ namespace App\Http; class HttpResponse { - /** * @var array */ diff --git a/src/Model/AuthModel.php b/src/Model/AuthModel.php index a12ce9e..1739ab5 100644 --- a/src/Model/AuthModel.php +++ b/src/Model/AuthModel.php @@ -69,7 +69,7 @@ class AuthModel { return null; } - return $this->gateway->getAccount($email); + return $this->gateway->getAccountFromMail($email); } diff --git a/src/Model/TacticModel.php b/src/Model/TacticModel.php index 73cfd91..8111963 100644 --- a/src/Model/TacticModel.php +++ b/src/Model/TacticModel.php @@ -2,8 +2,10 @@ namespace App\Model; +use App\Data\Account; use App\Data\TacticInfo; use App\Gateway\TacticInfoGateway; +use App\Validation\ValidationFail; class TacticModel { public const TACTIC_DEFAULT_NAME = "Nouvelle tactique"; @@ -39,15 +41,22 @@ class TacticModel { * Update the name of a tactic * @param int $id the tactic identifier * @param string $name the new name to set - * @return bool true if the update was done successfully + * @return ValidationFail[] failures, if any */ - public function updateName(int $id, string $name): bool { - if ($this->tactics->get($id) == null) { - return false; + public function updateName(int $id, string $name, int $authId): array { + + $tactic = $this->tactics->get($id); + + if ($tactic == null) { + return [ValidationFail::notFound("Could not find tactic")]; + } + + if ($tactic->getOwnerId() != $authId) { + return [ValidationFail::unauthorized()]; } $this->tactics->updateName($id, $name); - return true; + return []; } } diff --git a/src/Session/MutableSessionHandle.php b/src/Session/MutableSessionHandle.php index e142049..2bcb0fc 100644 --- a/src/Session/MutableSessionHandle.php +++ b/src/Session/MutableSessionHandle.php @@ -5,6 +5,5 @@ namespace App\Session; use App\Data\Account; interface MutableSessionHandle extends SessionHandle { - public function setAccount(Account $account): void; -} \ No newline at end of file +} diff --git a/src/Session/PhpSessionHandle.php b/src/Session/PhpSessionHandle.php index ea0e7c2..09d862c 100644 --- a/src/Session/PhpSessionHandle.php +++ b/src/Session/PhpSessionHandle.php @@ -5,8 +5,7 @@ namespace App\Session; use App\Data\Account; class PhpSessionHandle implements MutableSessionHandle { - - public static function init(): PhpSessionHandle { + public static function init(): SessionHandle { if (session_status() !== PHP_SESSION_NONE) { throw new \Exception("A php session is already started !"); } @@ -21,4 +20,4 @@ class PhpSessionHandle implements MutableSessionHandle { public function setAccount(Account $account): void { $_SESSION["account"] = $account; } -} \ No newline at end of file +} diff --git a/src/Session/SessionHandle.php b/src/Session/SessionHandle.php index 5a65794..6ffb9c0 100644 --- a/src/Session/SessionHandle.php +++ b/src/Session/SessionHandle.php @@ -5,7 +5,5 @@ namespace App\Session; use App\Data\Account; interface SessionHandle { - public function getAccount(): ?Account; - -} \ No newline at end of file +} diff --git a/src/Validation/ValidationFail.php b/src/Validation/ValidationFail.php index 83d6c76..2229a53 100644 --- a/src/Validation/ValidationFail.php +++ b/src/Validation/ValidationFail.php @@ -37,4 +37,8 @@ class ValidationFail implements JsonSerializable { return new ValidationFail("Not found", $message); } + public static function unauthorized(string $message = "Unauthorized"): ValidationFail { + return new ValidationFail("Unauthorized", $message); + } + } diff --git a/src/Validator/TacticValidator.php b/src/Validator/TacticValidator.php index 711fb72..e39cd51 100644 --- a/src/Validator/TacticValidator.php +++ b/src/Validator/TacticValidator.php @@ -6,17 +6,16 @@ use App\Data\TacticInfo; use App\Validation\ValidationFail; class TacticValidator { - - public static function validateAccess(?TacticInfo $tactic, int $ownerId): ?ValidationFail { + public static function validateAccess(?TacticInfo $tactic, int $tacticId, int $ownerId): ?ValidationFail { if ($tactic == null) { - return ValidationFail::notFound("La tactique " . $tactic->getId() . " n'existe pas"); + return ValidationFail::notFound("La tactique $tacticId n'existe pas"); } if ($tactic->getOwnerId() != $ownerId) { - return new ValidationFail("Unauthorized", "Vous ne pouvez pas accéder à cette tactique.",); + return ValidationFail::unauthorized("Vous ne pouvez pas accéder à cette tactique."); } return null; } -} \ No newline at end of file +} From 262fa6180e9ea0b6ba992fdf86db58f23007a10d Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Wed, 22 Nov 2023 08:40:15 +0100 Subject: [PATCH 36/75] applied suggestions --- public/index.php | 2 +- src/Controller/TeamController.php | 10 +++------- src/Data/Color.php | 1 + src/Data/MemberRole.php | 4 ++-- src/Views/display_team.html.twig | 7 ++++++- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/public/index.php b/public/index.php index cc63837..abc1a50 100644 --- a/public/index.php +++ b/public/index.php @@ -46,7 +46,7 @@ $router->map("POST","/team/new", fn()=>$teamController->SubmitTeam($_POST)); $router->map("GET","/team/list", fn()=>$teamController->displayListTeamByName()); $router->map("POST","/team/list", fn()=>$teamController->ListTeamByName($_POST)); -$router->map("GET","/team/[i:id]", fn(int $id)=>$teamController->displayTeam($id)); +$router->map("GET","/team/[i:id]", fn(int $id)=>$teamController->getTeam($id)); $match = $router->match(); diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index 288e733..d4c7bfe 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -13,15 +13,12 @@ use \Twig\Environment; class TeamController { private TeamModel $model; - private Environment $twig; /** * @param TeamModel $model - * @param Environment $twig */ - public function __construct(TeamModel $model, Environment $twig) { + public function __construct(TeamModel $model) { $this->model = $model; - $this->twig = $twig; } public function displaySubmitTeam() : HttpResponse { @@ -29,7 +26,6 @@ class TeamController } public function submitTeam(array $request): HttpResponse { - $errors = []; $request = HttpRequest::from($request, $errors, [ @@ -47,7 +43,7 @@ class TeamController } return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]); } - return $this->displayTeam($this->model->createTeam($request['name'], $request['picture'], $request['mainColor'], $request['secondColor'])); + return $this->getTeam($this->model->createTeam($request['name'], $request['picture'], $request['mainColor'], $request['secondColor'])); } public function displayListTeamByName(): HttpResponse { @@ -74,7 +70,7 @@ class TeamController return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $results]); } - public function displayTeam(int $id): HttpResponse { + public function getTeam(int $id): HttpResponse { $result = $this->model->displayTeam($id); return ViewHttpResponse::twig('display_team.html.twig', ['team' => $result]); } diff --git a/src/Data/Color.php b/src/Data/Color.php index 2e934c0..d32d81f 100755 --- a/src/Data/Color.php +++ b/src/Data/Color.php @@ -28,6 +28,7 @@ class Color { public static function from(string $value): Color { $color = self::tryFrom($value); if ($color == null) { + var_dump($value); throw new InvalidArgumentException("The string is not an hexadecimal code"); } return $color; diff --git a/src/Data/MemberRole.php b/src/Data/MemberRole.php index bd8add3..3bc9d56 100755 --- a/src/Data/MemberRole.php +++ b/src/Data/MemberRole.php @@ -12,8 +12,8 @@ use http\Exception\InvalidArgumentException; */ final class MemberRole { - public const ROLE_PLAYER = 0; - public const ROLE_COACH = 1; + private const ROLE_PLAYER = 0; + private const ROLE_COACH = 1; private const MIN = self::ROLE_PLAYER; private const MAX = self::ROLE_COACH; diff --git a/src/Views/display_team.html.twig b/src/Views/display_team.html.twig index f7bb02b..9783fe2 100644 --- a/src/Views/display_team.html.twig +++ b/src/Views/display_team.html.twig @@ -8,10 +8,11 @@ background-color: #f1f1f1; display: flex; flex-direction: column; + align-items: center; } section{ - width: 80%; + width: 60%; } .square{ @@ -32,6 +33,10 @@ .team{ border-color: darkgrey; border-radius: 20px; + align-items: center; + } + .team.h1{ + } From 65151cc1eacf00bea776f3c8666536c9c7846770 Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Wed, 22 Nov 2023 09:02:30 +0100 Subject: [PATCH 37/75] add some css --- src/Views/display_team.html.twig | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/Views/display_team.html.twig b/src/Views/display_team.html.twig index 9783fe2..732fd19 100644 --- a/src/Views/display_team.html.twig +++ b/src/Views/display_team.html.twig @@ -29,14 +29,27 @@ .container{ background-color: #fff; + display: flex; + flex-direction: column; + align-items: center; } .team{ border-color: darkgrey; border-radius: 20px; - align-items: center; + } - .team.h1{ + .colors{ + justify-content: space-between; + } + .color{ + flex-direction: row; + justify-content: space-between; + } + + .logo{ + height: 80px; + width: 80px; } @@ -47,11 +60,16 @@
-
-

{{ team.name }}

- Logo d'équipe -
-
+
+
+

{{ team.name }}

+ +
+
+

Couleur principale :

+

Couleur secondaire :

+
+ {% for m in team.members %}

m.id

From 69be9399549b2855bb01c715de63fa685c78c622 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 22 Nov 2023 09:38:35 +0100 Subject: [PATCH 38/75] add target url in session to bring back user to its initial target after auth --- public/api/index.php | 3 ++- public/index.php | 4 ++-- src/Controller/FrontController.php | 11 +++++++---- src/Controller/Sub/AuthController.php | 7 +++++-- src/Session/MutableSessionHandle.php | 3 +++ src/Session/PhpSessionHandle.php | 8 ++++++++ src/Session/SessionHandle.php | 2 ++ 7 files changed, 29 insertions(+), 9 deletions(-) diff --git a/public/api/index.php b/public/api/index.php index 8cc7287..580f794 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -16,6 +16,7 @@ use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; use App\Model\AuthModel; use App\Model\TacticModel; +use App\Session\PhpSessionHandle; use App\Session\SessionHandle; use App\Validation\ValidationFail; @@ -113,7 +114,7 @@ function tryGetAuthAccount(): ?Account { // If no authorization header is set, try fallback to php session. if (!isset($headers['Authorization'])) { - $session = SessionHandle::init(); + $session = PhpSessionHandle::init(); return $session->getAccount(); } diff --git a/public/index.php b/public/index.php index 7d42305..ce1df33 100644 --- a/public/index.php +++ b/public/index.php @@ -8,9 +8,9 @@ require "utils.php"; require "../src/react-display.php"; use App\Controller\FrontController; -use App\Session\SessionHandle; +use App\Session\PhpSessionHandle; $basePath = get_public_path(); $frontController = new FrontController($basePath); -$frontController->run(SessionHandle::init()); +$frontController->run(PhpSessionHandle::init()); diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index d1bbd6d..4ac3047 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -40,11 +40,12 @@ class FrontController { $match = $this->router->match(); if ($match) { $this->handleMatch($match, $session); - } else { - $this->displayViewByKind(ViewHttpResponse::twig("error.html.twig", [ - 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")], - ], HttpCodes::NOT_FOUND)); + return; } + + $this->displayViewByKind(ViewHttpResponse::twig("error.html.twig", [ + 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")], + ], HttpCodes::NOT_FOUND)); } /** @@ -96,6 +97,8 @@ class FrontController { if ($controllerName != self::VISITOR_CONTROLLER) { $account = $session->getAccount(); if ($account == null) { + // put in the session the initial url the user wanted to get + $session->setInitialTarget($_SERVER['REQUEST_URI']); return HttpResponse::redirect($this->basePath . "/visitor/login"); } } diff --git a/src/Controller/Sub/AuthController.php b/src/Controller/Sub/AuthController.php index fcf12d5..d94da43 100644 --- a/src/Controller/Sub/AuthController.php +++ b/src/Controller/Sub/AuthController.php @@ -63,7 +63,8 @@ class AuthController { $session->setAccount($account); - return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $account->getName(), 'email' => $account->getEmail()]); + $target_url = $session->getInitialTarget(); + return HttpResponse::redirect($target_url ?? "/home"); } @@ -92,7 +93,9 @@ class AuthController { $session->setAccount($account); - return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $account->getName(), 'email' => $account->getEmail()]); + $target_url = $session->getInitialTarget(); + $session->setInitialTarget(null); + return HttpResponse::redirect($target_url ?? "/home"); } } diff --git a/src/Session/MutableSessionHandle.php b/src/Session/MutableSessionHandle.php index 2bcb0fc..64ee52f 100644 --- a/src/Session/MutableSessionHandle.php +++ b/src/Session/MutableSessionHandle.php @@ -5,5 +5,8 @@ namespace App\Session; use App\Data\Account; interface MutableSessionHandle extends SessionHandle { + + public function setInitialTarget(?string $url): void; + public function setAccount(Account $account): void; } diff --git a/src/Session/PhpSessionHandle.php b/src/Session/PhpSessionHandle.php index 09d862c..5568eb5 100644 --- a/src/Session/PhpSessionHandle.php +++ b/src/Session/PhpSessionHandle.php @@ -17,7 +17,15 @@ class PhpSessionHandle implements MutableSessionHandle { return $_SESSION["account"] ?? null; } + public function getInitialTarget(): ?string { + return $_SESSION["target"] ?? null; + } + public function setAccount(Account $account): void { $_SESSION["account"] = $account; } + + public function setInitialTarget(?string $url): void { + $_SESSION["target"] = $url; + } } diff --git a/src/Session/SessionHandle.php b/src/Session/SessionHandle.php index 6ffb9c0..2114877 100644 --- a/src/Session/SessionHandle.php +++ b/src/Session/SessionHandle.php @@ -5,5 +5,7 @@ namespace App\Session; use App\Data\Account; interface SessionHandle { + public function getInitialTarget(): ?string; + public function getAccount(): ?Account; } From 55c67cc484d71af0418f17da315c8acb1c655831 Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Wed, 22 Nov 2023 09:46:28 +0100 Subject: [PATCH 39/75] Add a basic display of the tactics in the home page --- src/Controller/UserController.php | 18 ++++++++++- src/Gateway/TacticInfoGateway.php | 17 +++++++++++ src/Model/TacticModel.php | 4 +++ src/Views/home.css | 6 ++++ src/Views/home.twig | 51 +++++++++++++++++++++---------- 5 files changed, 79 insertions(+), 17 deletions(-) create mode 100644 src/Views/home.css diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 930cd67..d93a49b 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -2,12 +2,28 @@ namespace App\Controller; +use App\Connexion; +use App\Gateway\AuthGateway; +use App\Gateway\TacticInfoGateway; use App\Http\HttpResponse; use App\Http\ViewHttpResponse; +use App\Model\AuthModel; +use App\Model\TacticModel; class UserController { + private TacticModel $tacticMdl; + private AuthModel $accountMdl; + + public function __construct() + { + $con = new Connexion(get_database()); + $this->tacticMdl = new TacticModel(new TacticInfoGateway($con)); + $this->accountMdl = new AuthModel(new AuthGateway($con)); + } + public function home(): HttpResponse { - return ViewHttpResponse::twig("home.twig", []); + $listTactic = $this->tacticMdl->getLast(5); + return ViewHttpResponse::twig("home.twig", ["recentTactic" => $listTactic]); } public function default(): HttpResponse { diff --git a/src/Gateway/TacticInfoGateway.php b/src/Gateway/TacticInfoGateway.php index 3441c9a..4806a04 100644 --- a/src/Gateway/TacticInfoGateway.php +++ b/src/Gateway/TacticInfoGateway.php @@ -31,6 +31,23 @@ class TacticInfoGateway { return new TacticInfo($id, $row["name"], strtotime($row["creation_date"])); } + /** + * Return the nb last tactics created + * + * @param integer $nb + * @return array|null + */ + public function getLast(int $nb) : ?array { + $res = $this->con->fetch( + "SELECT * FROM TacticInfo ORDER BY creation_date DESC LIMIT :nb ", + [":nb" => [$nb, PDO::PARAM_INT]] + ); + if (count($res) == 0) { + return null; + } + return $res; + } + public function insert(string $name): TacticInfo { $this->con->exec( "INSERT INTO TacticInfo(name) VALUES(:name)", diff --git a/src/Model/TacticModel.php b/src/Model/TacticModel.php index cabcdec..d696b11 100644 --- a/src/Model/TacticModel.php +++ b/src/Model/TacticModel.php @@ -35,6 +35,10 @@ class TacticModel { return $this->tactics->get($id); } + public function getLast(int $nb) : ?array { + return $this->tactics->getLast($nb); + } + /** * Update the name of a tactic * @param int $id the tactic identifier diff --git a/src/Views/home.css b/src/Views/home.css new file mode 100644 index 0000000..c642b09 --- /dev/null +++ b/src/Views/home.css @@ -0,0 +1,6 @@ +.bandeau { + background-color: red; +} +body { + background-color: blue; +} \ No newline at end of file diff --git a/src/Views/home.twig b/src/Views/home.twig index 600ed1d..b7cc7d3 100644 --- a/src/Views/home.twig +++ b/src/Views/home.twig @@ -5,30 +5,49 @@ - Document + Page d'accueil + -

Page Home à faire

+
+

IQ Ball

+

Profil

+
-

Mes équipes

+

Mes équipes

-{% for team in recentTeam %} -
-

{{team.name}}

-
-{% endfor %} -

Mes strategies

+ {% for team in recentTeam %} +
+

{{team.name}}

+
+ {% endfor %} - +

Mes strategies

+ + + + {% if recentTactic != null %} + {% for tactic in recentTactic %} +
+

{{tactic.id}} - {{tactic.name}} - {{tactic.creation_date}}

+ +
+ {% endfor %} + {% else %} +

Aucune tactique créé !

+ {% endif %} -{% for tactic in recentTactic %} -
-

{{tactic.id}} - {{tactic.name}} - {{tactic.creation_date}}

-
-{% endfor %} -

+

From 66410afa2056917a08e32ded7d8ee8b78e2d1bca Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Wed, 22 Nov 2023 09:53:11 +0100 Subject: [PATCH 40/75] Test recentTeam empty in view --- src/Views/home.twig | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Views/home.twig b/src/Views/home.twig index b7cc7d3..500c02a 100644 --- a/src/Views/home.twig +++ b/src/Views/home.twig @@ -22,14 +22,17 @@

Profil

-

Mes équipes

+

Mes équipes

- - {% for team in recentTeam %} -
-

{{team.name}}

-
- {% endfor %} + {% if recentTeam != null %} + {% for team in recentTeam %} +
+

{{team.name}}

+
+ {% endfor %} + {% else %} +

Aucune équipe créé !

+ {% endif %}

Mes strategies

From 76e64a5ae5511f7f1198358f6fb67564225558de Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Wed, 22 Nov 2023 12:23:45 +0100 Subject: [PATCH 41/75] formated the code and verification plus some more css --- public/index.php | 12 +++++----- src/Controller/TeamController.php | 20 +++++++++------- src/Data/Color.php | 13 +++++----- src/Data/MemberRole.php | 5 ++-- src/Data/Team.php | 2 +- src/Gateway/TeamGateway.php | 40 +++++++++++++++++++++---------- src/Model/TeamModel.php | 5 ++-- src/Validation/Validators.php | 7 ++---- src/Views/display_team.html.twig | 13 +++++----- 9 files changed, 66 insertions(+), 51 deletions(-) diff --git a/public/index.php b/public/index.php index 3e4bf87..6633860 100644 --- a/public/index.php +++ b/public/index.php @@ -49,14 +49,14 @@ $router->map("GET", "/tactic/new", fn() => $editorController->makeNew()); $router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->openEditorFor($id)); $router->map("GET", "/tactic/[i:id]", fn(int $id) => $visualizerController->openVisualizer($id)); -$teamController = new \App\Controller\TeamController(new \App\Model\TeamModel(new \App\Gateway\TeamGateway($con)),$twig); -$router->map("GET","/team/new", fn()=>$teamController->displaySubmitTeam()); -$router->map("POST","/team/new", fn()=>$teamController->SubmitTeam($_POST)); +$teamController = new \App\Controller\TeamController(new \App\Model\TeamModel(new \App\Gateway\TeamGateway($con))); +$router->map("GET", "/team/new", fn() => $teamController->displaySubmitTeam()); +$router->map("POST", "/team/new", fn() => $teamController->SubmitTeam($_POST)); -$router->map("GET","/team/list", fn()=>$teamController->displayListTeamByName()); -$router->map("POST","/team/list", fn()=>$teamController->ListTeamByName($_POST)); +$router->map("GET", "/team/list", fn() => $teamController->displayListTeamByName()); +$router->map("POST", "/team/list", fn() => $teamController->ListTeamByName($_POST)); -$router->map("GET","/team/[i:id]", fn(int $id)=>$teamController->getTeam($id)); +$router->map("GET", "/team/[i:id]", fn(int $id) => $teamController->getTeam($id)); $match = $router->match(); diff --git a/src/Controller/TeamController.php b/src/Controller/TeamController.php index d4c7bfe..08e22cc 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/TeamController.php @@ -8,10 +8,8 @@ use App\Http\ViewHttpResponse; use App\Model\TeamModel; use App\Validation\FieldValidationFail; use App\Validation\Validators; -use \Twig\Environment; -class TeamController -{ +class TeamController { private TeamModel $model; /** @@ -21,10 +19,14 @@ class TeamController $this->model = $model; } - public function displaySubmitTeam() : HttpResponse { + public function displaySubmitTeam(): HttpResponse { return ViewHttpResponse::twig("insert_team.html.twig", []); } + /** + * @param array $request + * @return HttpResponse + */ public function submitTeam(array $request): HttpResponse { $errors = []; @@ -32,7 +34,7 @@ class TeamController "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], "mainColor" => [Validators::regex('/#(?:[0-9a-fA-F]{6})/')], "secondColor" => [Validators::regex('/#(?:[0-9a-fA-F]{6})/')], - "picture" => [Validators::isURL()] + "picture" => [Validators::isURL()], ]); if (!empty($errors)) { $badFields = []; @@ -50,10 +52,14 @@ class TeamController return ViewHttpResponse::twig("list_team_by_name.html.twig", []); } + /** + * @param array $request + * @return HttpResponse + */ public function listTeamByName(array $request): HttpResponse { $errors = []; $request = HttpRequest::from($request, $errors, [ - "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()] + "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], ]); if (!empty($errors) && $errors[0] instanceof FieldValidationFail) { @@ -75,5 +81,3 @@ class TeamController return ViewHttpResponse::twig('display_team.html.twig', ['team' => $result]); } } - - diff --git a/src/Data/Color.php b/src/Data/Color.php index 875b615..b12e27a 100755 --- a/src/Data/Color.php +++ b/src/Data/Color.php @@ -2,31 +2,31 @@ namespace App\Data; -use \InvalidArgumentException; +use InvalidArgumentException; class Color { /** - * @var string 6 bytes unsigned int that represents an RGB color + * @var string that represents an hexadecimal color code */ private string $hex; /** * @param string $value 6 bytes unsigned int that represents an RGB color - * @throws \InvalidArgumentException if the value is negative or greater than 0xFFFFFF + * @throws InvalidArgumentException if the value is negative or greater than 0xFFFFFF */ private function __construct(string $value) { if ($value < 0 || $value > 0xFFFFFF) { throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF"); } - $this->value = $value; + $this->hex = $value; } /** * @return string */ public function getValue(): string { - return $this->value; + return $this->hex; } public static function from(string $value): Color { @@ -39,10 +39,9 @@ class Color { } public static function tryFrom(string $value): ?Color { - if (!preg_match('/#(?:[0-9a-fA-F]{6})/',$value)) { + if (!preg_match('/#(?:[0-9a-fA-F]{6})/', $value)) { return null; } return new Color($value); } } - diff --git a/src/Data/MemberRole.php b/src/Data/MemberRole.php index 9838b92..559d516 100755 --- a/src/Data/MemberRole.php +++ b/src/Data/MemberRole.php @@ -10,7 +10,6 @@ use http\Exception\InvalidArgumentException; * encapsulates an integer value and use it as an enumeration discriminant */ final class MemberRole { - private const ROLE_PLAYER = 0; private const ROLE_COACH = 1; private const MIN = self::ROLE_PLAYER; @@ -25,11 +24,11 @@ final class MemberRole { $this->value = $val; } - public static function player(){ + public static function player(): MemberRole { return new MemberRole(MemberRole::ROLE_PLAYER); } - public static function coach(){ + public static function coach(): MemberRole { return new MemberRole(MemberRole::ROLE_COACH); } diff --git a/src/Data/Team.php b/src/Data/Team.php index a31829f..e50ad40 100755 --- a/src/Data/Team.php +++ b/src/Data/Team.php @@ -21,7 +21,7 @@ class Team { * @param Color $secondColor * @param Member[] $members */ - public function __construct(int $id,string $name, string $picture, Color $mainColor, Color $secondColor, array $members =[]) { + public function __construct(int $id, string $name, string $picture, Color $mainColor, Color $secondColor, array $members = []) { $this->id = $id; $this->name = $name; $this->picture = $picture; diff --git a/src/Gateway/TeamGateway.php b/src/Gateway/TeamGateway.php index 1cecbe1..c3d22dc 100644 --- a/src/Gateway/TeamGateway.php +++ b/src/Gateway/TeamGateway.php @@ -12,52 +12,68 @@ class TeamGateway { $this->con = $con; } - public function insert(string $name, string $picture, string $mainColor, string $secondColor) { + public function insert(string $name, string $picture, string $mainColor, string $secondColor): void { $this->con->exec( "INSERT INTO Team(name, picture, mainColor, secondColor) VALUES (:teamName , :picture, :mainColor, :secondColor)", [ ":teamName" => [$name, PDO::PARAM_STR], ":picture" => [$picture, PDO::PARAM_STR], ":mainColor" => [$mainColor, PDO::PARAM_STR], - ":secondColor" => [$secondColor, PDO::PARAM_STR] + ":secondColor" => [$secondColor, PDO::PARAM_STR], ] ); } + /** + * @param string $name + * @return array[] + */ public function listByName(string $name): array { return $this->con->fetch( "SELECT id,name,picture,mainColor,secondColor FROM Team WHERE name LIKE '%' || :name || '%'", [ - ":name" => [$name, PDO::PARAM_STR] + ":name" => [$name, PDO::PARAM_STR], ] ); } - public function getTeamById(int $id): array{ + /** + * @param int $id + * @return array[] + */ + public function getTeamById(int $id): array { return $this->con->fetch( "SELECT id,name,picture,mainColor,secondColor FROM Team WHERE id = :id", [ - ":id" => [$id, PDO::PARAM_INT] + ":id" => [$id, PDO::PARAM_INT], ] ); } - public function getIdTeamByName(string $name): array{ + /** + * @param string $name + * @return array[] + */ + public function getIdTeamByName(string $name): array { return $this->con->fetch( "SELECT id FROM Team WHERE name = :name", [ - ":name" => [$name, PDO::PARAM_STR] + ":name" => [$name, PDO::PARAM_STR], ] ); } - public function getMembersById($id):array{ + /** + * @param int $id + * @return array[] + */ + public function getMembersById(int $id): array { return $this->con->fetch( - "SELECT m.role,u.id FROM User u,Team t,Member m WHERE t.id = :id AND m.idTeam = t.id AND m.idMember = u.id", - [ - ":id" => [$id, PDO::PARAM_INT] + "SELECT m.role,u.id FROM User u,Team t,Member m WHERE t.id = :id AND m.idTeam = t.id AND m.idMember = u.id", + [ + ":id" => [$id, PDO::PARAM_INT], ] ); } -} \ No newline at end of file +} diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php index c5521eb..b6e4ab1 100644 --- a/src/Model/TeamModel.php +++ b/src/Model/TeamModel.php @@ -8,8 +8,7 @@ use App\Data\Member; use App\Data\MemberRole; use App\Data\Color; -class TeamModel -{ +class TeamModel { private TeamGateway $gateway; /** @@ -48,4 +47,4 @@ class TeamModel } return new Team(intval($result['id']), $result['name'], $result['picture'], Color::from($result['mainColor']), Color::from($result['secondColor']), $members); } -} \ No newline at end of file +} diff --git a/src/Validation/Validators.php b/src/Validation/Validators.php index b1d1eaf..cb28007 100644 --- a/src/Validation/Validators.php +++ b/src/Validation/Validators.php @@ -56,7 +56,7 @@ class Validators { return self::regex("/^[0-9]+$/"); } - public static function isIntInRange(int $min,int $max): Validator { + public static function isIntInRange(int $min, int $max): Validator { return new SimpleFunctionValidator( fn(string $val) => intval($val) >= $min && intval($val) <= $max, fn(string $name) => [new FieldValidationFail($name, "The value is not in the range $min to $max ")] @@ -65,11 +65,8 @@ class Validators { public static function isURL(): Validator { 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")] ); } } - - - diff --git a/src/Views/display_team.html.twig b/src/Views/display_team.html.twig index 732fd19..ada8566 100644 --- a/src/Views/display_team.html.twig +++ b/src/Views/display_team.html.twig @@ -22,9 +22,15 @@ #mainColor{ background-color: {{ team.mainColor.getValue() }}; + {% if team.mainColor.getValue() == "#ffffff" %} + border-color: #666666; + {% endif %} } #secondColor{ background-color: {{ team.secondColor.getValue() }}; + {% if team.secondColor.getValue() == "#ffffff" %} + border-color: #666666; + {% endif %} } .container{ @@ -39,9 +45,6 @@ } - .colors{ - justify-content: space-between; - } .color{ flex-direction: row; justify-content: space-between; @@ -65,12 +68,10 @@

{{ team.name }}

-
+

Couleur principale :

Couleur secondaire :

- - {% for m in team.members %}

m.id

{% endfor %} From d8000bcf3b32fbd97512f6740ea02eae9dc9cf80 Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Wed, 22 Nov 2023 12:33:27 +0100 Subject: [PATCH 42/75] fixing CI --- src/Model/TeamModel.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php index b6e4ab1..65f22a7 100644 --- a/src/Model/TeamModel.php +++ b/src/Model/TeamModel.php @@ -24,6 +24,10 @@ class TeamModel { return intval($result[0]['id']); } + /** + * @param string $name + * @return Team[] + */ public function listByName(string $name): array { $teams = []; $results = $this->gateway->listByName($name); From e44ea4721ef5b9f227269ada097418e4629754e9 Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Wed, 22 Nov 2023 13:03:12 +0100 Subject: [PATCH 43/75] Welcome Page done --- public/img/welcomePage/account.png | Bin 0 -> 507 bytes public/img/welcomePage/account.svg | 1 + src/Views/home.css | 2 +- src/Views/home.twig | 49 +++++++++++++++++++++++------ 4 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 public/img/welcomePage/account.png create mode 100644 public/img/welcomePage/account.svg diff --git a/public/img/welcomePage/account.png b/public/img/welcomePage/account.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed3299083bc52c2f0fa7994ee58a8bc4cb80872 GIT binary patch literal 507 zcmV25Zg zH(#4B7cD{=|_c`Ze4&X!=@5HN#C|jLhiPtH;F@Sq1U)Y5Y zec9tJ#vni|@uhg*Wat3YqZB|i&IR^40)$AR2LtW&hXADI#ZC|PCD+i78Q~lv9e{t^ zp8CAm`qa+Lw?+^F9G&pm0N@cIJ2v@7-<5b6f!Cr- zUj+EE%c;&v)qs>?a7aBnr2yoeqQ@>G9o|I|WE5a5MF}TIMWpRh+(nlva>?TwL&z4H zs?(+vv+HB6ku&TnD=fy*X^i8rL(V%&wy9tAhlcWhh8^ia|0_7M^^)VL@HCVTJJ-*S z3Y6loBMsjBZeaLDhZI7S!_F^95W0&!BZRa;qywWA)!)LFaTKS1I2fiTIU?r(ty4-R xzf9@^Oy1~O&WHv5`c*S^2f6S`~nTSY#9m*AXNYW002ovPDHLkV1k67-3$N# literal 0 HcmV?d00001 diff --git a/public/img/welcomePage/account.svg b/public/img/welcomePage/account.svg new file mode 100644 index 0000000..ce59194 --- /dev/null +++ b/public/img/welcomePage/account.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Views/home.css b/src/Views/home.css index c642b09..1f4f995 100644 --- a/src/Views/home.css +++ b/src/Views/home.css @@ -2,5 +2,5 @@ background-color: red; } body { - background-color: blue; + } \ No newline at end of file diff --git a/src/Views/home.twig b/src/Views/home.twig index 500c02a..1c14e02 100644 --- a/src/Views/home.twig +++ b/src/Views/home.twig @@ -7,19 +7,56 @@ Page d'accueil

IQ Ball

-

Profil

+
+ Account logo +

Mon profil

+

Mes équipes

@@ -49,13 +86,5 @@

Aucune tactique créé !

{% endif %} - -

- - - - - - \ No newline at end of file From b489364cd21b461744bd7cc6196da3113e9c6b20 Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Wed, 22 Nov 2023 13:03:53 +0100 Subject: [PATCH 44/75] delete css file --- src/Views/home.css | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/Views/home.css diff --git a/src/Views/home.css b/src/Views/home.css deleted file mode 100644 index 1f4f995..0000000 --- a/src/Views/home.css +++ /dev/null @@ -1,6 +0,0 @@ -.bandeau { - background-color: red; -} -body { - -} \ No newline at end of file From e3875b4f15ce3b710187808420290445fe0b974c Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 22 Nov 2023 13:02:28 +0100 Subject: [PATCH 45/75] add documentation --- Documentation/database_mcd.puml | 17 +++++++++++------ public/api/index.php | 4 +++- sql/setup-tables.sql | 10 ++-------- src/Gateway/TacticInfoGateway.php | 8 ++++---- src/Session/MutableSessionHandle.php | 10 +++++++++- src/Session/PhpSessionHandle.php | 5 ++++- src/Session/SessionHandle.php | 12 ++++++++++++ 7 files changed, 45 insertions(+), 21 deletions(-) diff --git a/Documentation/database_mcd.puml b/Documentation/database_mcd.puml index e698a69..710dfee 100644 --- a/Documentation/database_mcd.puml +++ b/Documentation/database_mcd.puml @@ -2,12 +2,10 @@ object Account { id - name - age email - phoneNumber - passwordHash - profilePicture + username + token + hash } object Team { @@ -26,7 +24,7 @@ object TacticFolder { object Tactic { id_json name - creationDate + creation_date } usecase have_team [ @@ -63,6 +61,10 @@ usecase contains_other_folder [ to contain ] +usecase owns [ + owns +] + Account "0,n" -- have_team have_team -- "1,n" Team @@ -73,6 +75,9 @@ shared_tactic_account -- "0,n" Tactic Tactic "0,n" -- shared_tactic_team shared_tactic_team -- "0,n" Team +Tactic "1,1" -- owns +owns -- Account + Team "0,n" -- shared_folder_team shared_folder_team -- "0,n"TacticFolder diff --git a/public/api/index.php b/public/api/index.php index 580f794..c5f8f2e 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -17,7 +17,6 @@ use App\Http\ViewHttpResponse; use App\Model\AuthModel; use App\Model\TacticModel; use App\Session\PhpSessionHandle; -use App\Session\SessionHandle; use App\Validation\ValidationFail; function getTacticController(): APITacticController { @@ -28,6 +27,9 @@ function getAuthController(): APIAuthController { return new APIAuthController(new AuthModel(new AccountGateway(new Connexion(get_database())))); } +/** + * A Front controller action + */ class Action { /** * @var callable(mixed[]): HttpResponse $action action to call diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 457803c..26e58da 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -1,13 +1,7 @@ -- drop tables here -DROP TABLE IF EXISTS FormEntries; DROP TABLE IF EXISTS Account; -DROP TABLE IF EXISTS TacticInfo; +DROP TABLE IF EXISTS Tactic; -CREATE TABLE FormEntries -( - name varchar NOT NULL, - description varchar NOT NULL -); CREATE TABLE Account ( id integer PRIMARY KEY AUTOINCREMENT, @@ -17,7 +11,7 @@ CREATE TABLE Account hash varchar NOT NULL ); -CREATE TABLE TacticInfo +CREATE TABLE Tactic ( id integer PRIMARY KEY AUTOINCREMENT, name varchar NOT NULL, diff --git a/src/Gateway/TacticInfoGateway.php b/src/Gateway/TacticInfoGateway.php index efa0a7c..3075f69 100644 --- a/src/Gateway/TacticInfoGateway.php +++ b/src/Gateway/TacticInfoGateway.php @@ -18,7 +18,7 @@ class TacticInfoGateway { public function get(int $id): ?TacticInfo { $res = $this->con->fetch( - "SELECT * FROM TacticInfo WHERE id = :id", + "SELECT * FROM Tactic WHERE id = :id", [":id" => [$id, PDO::PARAM_INT]] ); @@ -33,14 +33,14 @@ class TacticInfoGateway { public function insert(string $name, int $owner): TacticInfo { $this->con->exec( - "INSERT INTO TacticInfo(name, owner) VALUES(:name, :owner)", + "INSERT INTO Tactic(name, owner) VALUES(:name, :owner)", [ ":name" => [$name, PDO::PARAM_STR], ":owner" => [$owner, PDO::PARAM_INT], ] ); $row = $this->con->fetch( - "SELECT id, creation_date, owner FROM TacticInfo WHERE :id = id", + "SELECT id, creation_date, owner FROM Tactic WHERE :id = id", [':id' => [$this->con->lastInsertId(), PDO::PARAM_INT]] )[0]; return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"]), $row["owner"]); @@ -48,7 +48,7 @@ class TacticInfoGateway { public function updateName(int $id, string $name): void { $this->con->exec( - "UPDATE TacticInfo SET name = :name WHERE id = :id", + "UPDATE Tactic SET name = :name WHERE id = :id", [ ":name" => [$name, PDO::PARAM_STR], ":id" => [$id, PDO::PARAM_INT], diff --git a/src/Session/MutableSessionHandle.php b/src/Session/MutableSessionHandle.php index 64ee52f..1459819 100644 --- a/src/Session/MutableSessionHandle.php +++ b/src/Session/MutableSessionHandle.php @@ -4,9 +4,17 @@ namespace App\Session; use App\Data\Account; +/** + * The mutable side of a session handle + */ interface MutableSessionHandle extends SessionHandle { - + /** + * @param string|null $url the url to redirect the user to after authentication. + */ public function setInitialTarget(?string $url): void; + /** + * @param Account $account update the session's account + */ public function setAccount(Account $account): void; } diff --git a/src/Session/PhpSessionHandle.php b/src/Session/PhpSessionHandle.php index 5568eb5..7d6b10b 100644 --- a/src/Session/PhpSessionHandle.php +++ b/src/Session/PhpSessionHandle.php @@ -4,8 +4,11 @@ namespace App\Session; use App\Data\Account; +/** + * A PHP session handle + */ class PhpSessionHandle implements MutableSessionHandle { - public static function init(): SessionHandle { + public static function init(): self { if (session_status() !== PHP_SESSION_NONE) { throw new \Exception("A php session is already started !"); } diff --git a/src/Session/SessionHandle.php b/src/Session/SessionHandle.php index 2114877..663c78c 100644 --- a/src/Session/SessionHandle.php +++ b/src/Session/SessionHandle.php @@ -4,8 +4,20 @@ namespace App\Session; use App\Data\Account; +/** + * An immutable session handle + */ interface SessionHandle { + /** + * The initial target url if the user wanted to perform an action that requires authentication + * but has been required to login first in the application. + * @return string|null Get the initial targeted URL + */ public function getInitialTarget(): ?string; + /** + * The session account if the user is logged in. + * @return Account|null + */ public function getAccount(): ?Account; } From a11dd2e1228299bd0947ce44131be4c21783ed37 Mon Sep 17 00:00:00 2001 From: Yanis DAHMANE-BOUNOUA Date: Tue, 21 Nov 2023 21:30:12 +0100 Subject: [PATCH 46/75] front-controller (#17) I did the FRONT CONTROLLERRRRR. (Salva version lol) When you create a new controller, define the path to call it here (image 1). Don't forget to associate a controller with a role. The main method have to return a `ViewHttpResponse`. If you create a new action, please make sure that the action's main method have the same name than the action (image 2 and 3) and to make the main method public. Co-authored-by: DahmaneYanis Co-authored-by: d_yanis Co-authored-by: Override-6 Reviewed-on: https://codefirst.iut.uca.fr/git/IQBall/Application-Web/pulls/17 Co-authored-by: Yanis DAHMANE-BOUNOUA Co-committed-by: Yanis DAHMANE-BOUNOUA --- Documentation/http.puml | 2 +- Documentation/php.puml | 8 ++ public/index.php | 90 +--------------- src/Controller/EditorController.php | 14 ++- src/Controller/FrontController.php | 162 ++++++++++++++++++++++++++++ src/Controller/UserController.php | 16 +++ src/Views/home.twig | 13 +++ 7 files changed, 210 insertions(+), 95 deletions(-) create mode 100644 Documentation/php.puml create mode 100644 src/Controller/FrontController.php create mode 100644 src/Controller/UserController.php create mode 100644 src/Views/home.twig diff --git a/Documentation/http.puml b/Documentation/http.puml index b41135d..9fbe606 100644 --- a/Documentation/http.puml +++ b/Documentation/http.puml @@ -35,7 +35,7 @@ class ViewHttpResponse extends HttpResponse { - arguments: array - kind: int - + __construct(kind: int, file: string, arguments: array, code: int = HttpCodes::OK) + - __construct(kind: int, file: string, arguments: array, code: int = HttpCodes::OK) + getViewKind(): int + getFile(): string + getArguments(): array diff --git a/Documentation/php.puml b/Documentation/php.puml new file mode 100644 index 0000000..8641900 --- /dev/null +++ b/Documentation/php.puml @@ -0,0 +1,8 @@ +@startuml + +class FrontController { + - router : AltoRouter + +} + +@enduml \ No newline at end of file diff --git a/public/index.php b/public/index.php index 6633860..f898d7f 100644 --- a/public/index.php +++ b/public/index.php @@ -4,92 +4,10 @@ require "../vendor/autoload.php"; require "../config.php"; require "../sql/database.php"; require "utils.php"; +require "../src/react-display.php"; -use App\Connexion; -use App\Controller\EditorController; -use App\Controller\SampleFormController; -use App\Gateway\FormResultGateway; -use App\Gateway\TacticInfoGateway; -use App\Http\JsonHttpResponse; -use App\Http\ViewHttpResponse; -use App\Model\TacticModel; -use Twig\Loader\FilesystemLoader; -use App\Gateway\AuthGateway; -use App\Controller\AuthController; -use App\Validation\ValidationFail; -use App\Controller\ErrorController; -use App\Controller\VisualizerController; - -$loader = new FilesystemLoader('../src/Views/'); -$twig = new \Twig\Environment($loader); +use App\Controller\FrontController; $basePath = get_public_path(); -$con = new Connexion(get_database()); - -// routes initialization -$router = new AltoRouter(); -$router->setBasePath($basePath); - -$sampleFormController = new SampleFormController(new FormResultGateway($con)); -$authGateway = new AuthGateway($con); -$authController = new \App\Controller\AuthController(new \App\Model\AuthModel($authGateway)); -$editorController = new EditorController(new TacticModel(new TacticInfoGateway($con))); -$visualizerController = new VisualizerController(new TacticModel(new TacticInfoGateway($con))); - - -$router->map("GET", "/", fn() => $sampleFormController->displayFormReact()); -$router->map("POST", "/submit", fn() => $sampleFormController->submitFormReact($_POST)); -$router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); -$router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); -$router->map("GET", "/register", fn() => $authController->displayRegister()); -$router->map("POST", "/register", fn() => $authController->confirmRegister($_POST)); -$router->map("GET", "/login", fn() => $authController->displayLogin()); -$router->map("POST", "/login", fn() => $authController->confirmLogin($_POST)); -$router->map("GET", "/tactic/new", fn() => $editorController->makeNew()); -$router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->openEditorFor($id)); -$router->map("GET", "/tactic/[i:id]", fn(int $id) => $visualizerController->openVisualizer($id)); - -$teamController = new \App\Controller\TeamController(new \App\Model\TeamModel(new \App\Gateway\TeamGateway($con))); -$router->map("GET", "/team/new", fn() => $teamController->displaySubmitTeam()); -$router->map("POST", "/team/new", fn() => $teamController->SubmitTeam($_POST)); - -$router->map("GET", "/team/list", fn() => $teamController->displayListTeamByName()); -$router->map("POST", "/team/list", fn() => $teamController->ListTeamByName($_POST)); - -$router->map("GET", "/team/[i:id]", fn(int $id) => $teamController->getTeam($id)); - -$match = $router->match(); - -if ($match == null) { - http_response_code(404); - ErrorController::displayFailures([ValidationFail::notFound("Cette page n'existe pas")], $twig); - return; -} - -$response = call_user_func_array($match['target'], $match['params']); - -http_response_code($response->getCode()); - -if ($response instanceof ViewHttpResponse) { - $file = $response->getFile(); - $args = $response->getArguments(); - - switch ($response->getViewKind()) { - case ViewHttpResponse::REACT_VIEW: - send_react_front($file, $args); - break; - case ViewHttpResponse::TWIG_VIEW: - try { - $twig->display($file, $args); - } catch (\Twig\Error\RuntimeError|\Twig\Error\SyntaxError $e) { - http_response_code(500); - echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); - throw $e; - } - break; - } - -} elseif ($response instanceof JsonHttpResponse) { - header('Content-type: application/json'); - echo $response->getJson(); -} +$frontController = new FrontController($basePath); +$frontController->run(); diff --git a/src/Controller/EditorController.php b/src/Controller/EditorController.php index ed270d1..eb07184 100644 --- a/src/Controller/EditorController.php +++ b/src/Controller/EditorController.php @@ -2,9 +2,10 @@ namespace App\Controller; +use App\Connexion; use App\Data\TacticInfo; +use App\Gateway\TacticInfoGateway; use App\Http\HttpCodes; -use App\Http\HttpRequest; use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; @@ -13,18 +14,15 @@ use App\Model\TacticModel; class EditorController { private TacticModel $model; - /** - * @param TacticModel $model - */ - public function __construct(TacticModel $model) { - $this->model = $model; + public function __construct() { + $this->model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); } private function openEditor(TacticInfo $tactic): HttpResponse { return ViewHttpResponse::react("views/Editor.tsx", ["name" => $tactic->getName(), "id" => $tactic->getId()]); } - public function makeNew(): HttpResponse { + public function create(): HttpResponse { $tactic = $this->model->makeNewDefault(); return $this->openEditor($tactic); } @@ -34,7 +32,7 @@ class EditorController { * @param int $id the targeted tactic identifier * @return HttpResponse */ - public function openEditorFor(int $id): HttpResponse { + public function edit(int $id): HttpResponse { $tactic = $this->model->get($id); if ($tactic == null) { diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php new file mode 100644 index 0000000..66d5d4f --- /dev/null +++ b/src/Controller/FrontController.php @@ -0,0 +1,162 @@ +router = $this->createRouter($basePath); + $this->initializeRouterMap(); + } + + /** + * Main behavior of the FrontController + * + * @return void + */ + public function run(): void { + $match = $this->router->match(); + if ($match != null) { + $this->handleMatch($match); + } else { + $this->displayViewByKind(ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND)); + } + } + + /** + * Create a new instance of an AltoRouter + * + * @param string $basePath + * @return AltoRouter + */ + public function createRouter(string $basePath): AltoRouter { + $router = new AltoRouter(); + $router->setBasePath($basePath); + return $router; + } + + /** + * Initialize project's routes + * + * @return void + */ + private function initializeRouterMap(): void { + $this->router->map("GET", "/", "UserController"); + $this->router->map("GET", "/[a:action]?", "UserController"); + $this->router->map("GET", "/tactic/[a:action]/[i:idTactic]?", "EditorController"); + } + + /** + * @param array $match + * @return void + */ + private function handleMatch(array $match): void { + $tag = $match['target']; + + $action = $this->getAction($match); + $params = $match["params"]; + unset($params['action']); + $this->handleResponseByType($this->tryToCall($tag, $action, array_values($params))); + } + + /** + * @param string $controller + * @param string $action + * @param array $params + * @return HttpResponse + */ + private function tryToCall(string $controller, string $action, array $params): HttpResponse { + $controller = $this->getController($controller); + try { + if (is_callable([$controller, $action])) { + return call_user_func_array([$controller, $action], $params); + } else { + return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); + } + } catch (Exception $e) { + return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); + } + } + + /** + * Get the right method to call to do an action + * + * @param array $match + * @return string + */ + private function getAction(array $match): string { + if (isset($match["params"]["action"])) { + return $match["params"]["action"]; + } + return "default"; + } + + /** + * Initialize the right controller by the user's role + * + * @param string $controller + * @return mixed + */ + private function getController(string $controller) { + $namespace = "\\App\\Controller\\"; + $controller = $namespace . $controller; + return new $controller(); + } + + /** + * Redirect the return by the response's type + * + * @param HttpResponse $response + * @return void + */ + private function handleResponseByType(HttpResponse $response): void { + http_response_code($response->getCode()); + if ($response instanceof ViewHttpResponse) { + $this->displayViewByKind($response); + } elseif ($response instanceof JsonHttpResponse) { + header('Content-type: application/json'); + echo $response->getJson(); + } + } + + /** + * Use the right method to display the response + * + * @param ViewHttpResponse $response + * @return void + */ + private function displayViewByKind(ViewHttpResponse $response): void { + $file = $response->getFile(); + $args = $response->getArguments(); + + switch ($response->getViewKind()) { + case ViewHttpResponse::REACT_VIEW: + send_react_front($file, $args); + break; + case ViewHttpResponse::TWIG_VIEW: + try { + $loader = new FilesystemLoader('../src/Views/'); + $twig = new Environment($loader); + $twig->display($file, $args); + } catch (RuntimeError|SyntaxError|LoaderError $e) { + http_response_code(500); + echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); + throw $e; + } + break; + } + } +} diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php new file mode 100644 index 0000000..930cd67 --- /dev/null +++ b/src/Controller/UserController.php @@ -0,0 +1,16 @@ + + + + + + + Document + + +

Page Home à faire

+ + \ No newline at end of file From 038686e2ed2fcab7fb461bf37cc75ee42f7f2d18 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Tue, 21 Nov 2023 22:57:36 +0100 Subject: [PATCH 47/75] add salva-like routes and controller architecture --- src/Controller/ErrorController.php | 26 -------- src/Controller/FrontController.php | 2 +- src/Controller/SampleFormController.php | 64 ------------------- src/Controller/{ => Sub}/AuthController.php | 4 +- src/Controller/{ => Sub}/EditorController.php | 4 +- .../{ => Sub}/VisualizerController.php | 5 +- src/Controller/UserController.php | 36 ++++++++++- src/Model/AuthModel.php | 1 - 8 files changed, 40 insertions(+), 102 deletions(-) delete mode 100644 src/Controller/ErrorController.php delete mode 100644 src/Controller/SampleFormController.php rename src/Controller/{ => Sub}/AuthController.php (97%) rename src/Controller/{ => Sub}/EditorController.php (93%) rename src/Controller/{ => Sub}/VisualizerController.php (88%) diff --git a/src/Controller/ErrorController.php b/src/Controller/ErrorController.php deleted file mode 100644 index 7fc5239..0000000 --- a/src/Controller/ErrorController.php +++ /dev/null @@ -1,26 +0,0 @@ -display("error.html.twig", ['failures' => $failures]); - } catch (LoaderError|RuntimeError|SyntaxError $e) { - echo "Twig error: $e"; - } - } -} diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index 66d5d4f..0600a01 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -56,7 +56,7 @@ class FrontController { private function initializeRouterMap(): void { $this->router->map("GET", "/", "UserController"); $this->router->map("GET", "/[a:action]?", "UserController"); - $this->router->map("GET", "/tactic/[a:action]/[i:idTactic]?", "EditorController"); + $this->router->map("GET", "/tactic/[a:action]/[i:idTactic]?", "UserController"); } /** diff --git a/src/Controller/SampleFormController.php b/src/Controller/SampleFormController.php deleted file mode 100644 index 773a4db..0000000 --- a/src/Controller/SampleFormController.php +++ /dev/null @@ -1,64 +0,0 @@ -gateway = $gateway; - } - - - public function displayFormReact(): HttpResponse { - return ViewHttpResponse::react("views/SampleForm.tsx", []); - } - - public function displayFormTwig(): HttpResponse { - return ViewHttpResponse::twig('sample_form.html.twig', []); - } - - /** - * @param array $form - * @param callable(array>): ViewHttpResponse $response - * @return HttpResponse - */ - private function submitForm(array $form, callable $response): HttpResponse { - return Control::runCheckedFrom($form, [ - "name" => [Validators::lenBetween(0, 32), Validators::name("Le nom ne peut contenir que des lettres, des chiffres et des accents")], - "description" => [Validators::lenBetween(0, 512)], - ], function (HttpRequest $req) use ($response) { - $description = htmlspecialchars($req["description"]); - $this->gateway->insert($req["name"], $description); - $results = ["results" => $this->gateway->listResults()]; - return call_user_func_array($response, [$results]); - }, false); - } - - /** - * @param array $form - * @return HttpResponse - */ - public function submitFormTwig(array $form): HttpResponse { - return $this->submitForm($form, fn(array $results) => ViewHttpResponse::twig('display_results.html.twig', $results)); - } - - /** - * @param array $form - * @return HttpResponse - */ - public function submitFormReact(array $form): HttpResponse { - return $this->submitForm($form, fn(array $results) => ViewHttpResponse::react('views/DisplayResults.tsx', $results)); - } -} diff --git a/src/Controller/AuthController.php b/src/Controller/Sub/AuthController.php similarity index 97% rename from src/Controller/AuthController.php rename to src/Controller/Sub/AuthController.php index e42c27d..a78b02e 100644 --- a/src/Controller/AuthController.php +++ b/src/Controller/Sub/AuthController.php @@ -1,8 +1,7 @@ $tactic->getName(), "id" => $tactic->getId()]); } - public function create(): HttpResponse { + public function createNew(): HttpResponse { $tactic = $this->model->makeNewDefault(); return $this->openEditor($tactic); } diff --git a/src/Controller/VisualizerController.php b/src/Controller/Sub/VisualizerController.php similarity index 88% rename from src/Controller/VisualizerController.php rename to src/Controller/Sub/VisualizerController.php index 3c8b55e..e3b5663 100644 --- a/src/Controller/VisualizerController.php +++ b/src/Controller/Sub/VisualizerController.php @@ -1,6 +1,6 @@ tacticModel = $tacticModel; } - public function openVisualizer(int $id): HttpResponse { + public function visualize(int $id): HttpResponse { $tactic = $this->tacticModel->get($id); if ($tactic == null) { @@ -27,6 +27,5 @@ class VisualizerController { } return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]); - } } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 930cd67..2a95bcc 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -2,15 +2,47 @@ namespace App\Controller; +use App\Connexion; +use App\Gateway\AuthGateway; +use App\Gateway\TacticInfoGateway; use App\Http\HttpResponse; use App\Http\ViewHttpResponse; +use App\Model\AuthModel; +use App\Model\TacticModel; class UserController { public function home(): HttpResponse { return ViewHttpResponse::twig("home.twig", []); } - public function default(): HttpResponse { - return self::home(); + public function register(): HttpResponse { + $model = new AuthModel(new AuthGateway(new Connexion(get_database()))); + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + return (new Sub\AuthController($model))->displayRegister(); + } + return (new Sub\AuthController($model))->confirmRegister($_POST); + } + + public function login(): HttpResponse { + $model = new AuthModel(new AuthGateway(new Connexion(get_database()))); + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + return (new Sub\AuthController($model))->displayLogin(); + } + return (new Sub\AuthController($model))->confirmLogin($_POST); + } + + public function open(int $id): HttpResponse { + $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); + return (new Sub\VisualizerController($model))->visualize($id); + } + + public function edit(int $id): HttpResponse { + $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); + return (new Sub\EditorController($model))->edit($id); + } + + public function create(): HttpResponse { + $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); + return (new Sub\EditorController($model))->createNew(); } } diff --git a/src/Model/AuthModel.php b/src/Model/AuthModel.php index 45b63e4..312cca8 100644 --- a/src/Model/AuthModel.php +++ b/src/Model/AuthModel.php @@ -2,7 +2,6 @@ namespace App\Model; -use App\Controller\AuthController; use App\Gateway\AuthGateway; use App\Validation\FieldValidationFail; use App\Validation\ValidationFail; From 5b9096ab35f2cf765700146e7bc34040a27ebfc2 Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Wed, 22 Nov 2023 13:31:25 +0100 Subject: [PATCH 48/75] Correction CI --- src/Controller/UserController.php | 2 -- src/Gateway/TacticInfoGateway.php | 2 +- src/Model/TacticModel.php | 6 ++++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index d93a49b..7fe41ed 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -12,13 +12,11 @@ use App\Model\TacticModel; class UserController { private TacticModel $tacticMdl; - private AuthModel $accountMdl; public function __construct() { $con = new Connexion(get_database()); $this->tacticMdl = new TacticModel(new TacticInfoGateway($con)); - $this->accountMdl = new AuthModel(new AuthGateway($con)); } public function home(): HttpResponse { diff --git a/src/Gateway/TacticInfoGateway.php b/src/Gateway/TacticInfoGateway.php index 4806a04..a9ef5cc 100644 --- a/src/Gateway/TacticInfoGateway.php +++ b/src/Gateway/TacticInfoGateway.php @@ -35,7 +35,7 @@ class TacticInfoGateway { * Return the nb last tactics created * * @param integer $nb - * @return array|null + * @return TacticInfo[] */ public function getLast(int $nb) : ?array { $res = $this->con->fetch( diff --git a/src/Model/TacticModel.php b/src/Model/TacticModel.php index d696b11..aeef9bc 100644 --- a/src/Model/TacticModel.php +++ b/src/Model/TacticModel.php @@ -35,6 +35,12 @@ class TacticModel { return $this->tactics->get($id); } + /** + * Return the nb last tactics created + * + * @param integer $nb + * @return TacticInfo[] + */ public function getLast(int $nb) : ?array { return $this->tactics->getLast($nb); } From 88e547bfcbcbab2cefb05a130882794430fe0cfd Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Wed, 22 Nov 2023 13:32:56 +0100 Subject: [PATCH 49/75] Correction CI --- src/Gateway/TacticInfoGateway.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gateway/TacticInfoGateway.php b/src/Gateway/TacticInfoGateway.php index a9ef5cc..65c214d 100644 --- a/src/Gateway/TacticInfoGateway.php +++ b/src/Gateway/TacticInfoGateway.php @@ -35,7 +35,7 @@ class TacticInfoGateway { * Return the nb last tactics created * * @param integer $nb - * @return TacticInfo[] + * @return array> */ public function getLast(int $nb) : ?array { $res = $this->con->fetch( From c696eafa38107bb7e593b8f4683d30d6cec8f41e Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Wed, 22 Nov 2023 13:33:56 +0100 Subject: [PATCH 50/75] Correction CI --- src/Model/TacticModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/TacticModel.php b/src/Model/TacticModel.php index aeef9bc..809c915 100644 --- a/src/Model/TacticModel.php +++ b/src/Model/TacticModel.php @@ -39,7 +39,7 @@ class TacticModel { * Return the nb last tactics created * * @param integer $nb - * @return TacticInfo[] + * @return array> */ public function getLast(int $nb) : ?array { return $this->tactics->getLast($nb); From 851aa72e2c4e6ec8bc42a21cd0c3bb8fac996d64 Mon Sep 17 00:00:00 2001 From: "vivien.dufour" Date: Wed, 22 Nov 2023 08:38:58 +0100 Subject: [PATCH 51/75] fix on routes --- src/Controller/FrontController.php | 49 ++++++++++++--------- src/Controller/Sub/EditorController.php | 4 +- src/Controller/{ => Sub}/TeamController.php | 2 +- src/Controller/UserController.php | 26 +++++++++++ src/Views/display_teams.html.twig | 4 +- src/Views/insert_team.html.twig | 2 +- src/Views/list_team_by_name.html.twig | 2 +- 7 files changed, 61 insertions(+), 28 deletions(-) rename src/Controller/{ => Sub}/TeamController.php (98%) diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index 0600a01..7d61b28 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -14,10 +14,12 @@ use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Loader\FilesystemLoader; -class FrontController { +class FrontController +{ private AltoRouter $router; - public function __construct(string $basePath) { + public function __construct(string $basePath) + { $this->router = $this->createRouter($basePath); $this->initializeRouterMap(); } @@ -27,7 +29,8 @@ class FrontController { * * @return void */ - public function run(): void { + public function run(): void + { $match = $this->router->match(); if ($match != null) { $this->handleMatch($match); @@ -42,7 +45,8 @@ class FrontController { * @param string $basePath * @return AltoRouter */ - public function createRouter(string $basePath): AltoRouter { + public function createRouter(string $basePath): AltoRouter + { $router = new AltoRouter(); $router->setBasePath($basePath); return $router; @@ -53,17 +57,19 @@ class FrontController { * * @return void */ - private function initializeRouterMap(): void { + private function initializeRouterMap(): void + { $this->router->map("GET", "/", "UserController"); - $this->router->map("GET", "/[a:action]?", "UserController"); - $this->router->map("GET", "/tactic/[a:action]/[i:idTactic]?", "UserController"); + $this->router->map("GET|POST", "/[a:action]?/[i:id]", "UserController"); + $this->router->map("GET|POST", "/tactic/[a:action]/[i:idTactic]?", "UserController"); } /** * @param array $match * @return void */ - private function handleMatch(array $match): void { + private function handleMatch(array $match): void + { $tag = $match['target']; $action = $this->getAction($match); @@ -78,15 +84,12 @@ class FrontController { * @param array $params * @return HttpResponse */ - private function tryToCall(string $controller, string $action, array $params): HttpResponse { + private function tryToCall(string $controller, string $action, array $params): HttpResponse + { $controller = $this->getController($controller); - try { - if (is_callable([$controller, $action])) { - return call_user_func_array([$controller, $action], $params); - } else { - return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); - } - } catch (Exception $e) { + if (is_callable([$controller, $action])) { + return call_user_func_array([$controller, $action], $params); + } else { return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); } } @@ -97,7 +100,8 @@ class FrontController { * @param array $match * @return string */ - private function getAction(array $match): string { + private function getAction(array $match): string + { if (isset($match["params"]["action"])) { return $match["params"]["action"]; } @@ -110,7 +114,8 @@ class FrontController { * @param string $controller * @return mixed */ - private function getController(string $controller) { + private function getController(string $controller) + { $namespace = "\\App\\Controller\\"; $controller = $namespace . $controller; return new $controller(); @@ -122,7 +127,8 @@ class FrontController { * @param HttpResponse $response * @return void */ - private function handleResponseByType(HttpResponse $response): void { + private function handleResponseByType(HttpResponse $response): void + { http_response_code($response->getCode()); if ($response instanceof ViewHttpResponse) { $this->displayViewByKind($response); @@ -138,7 +144,8 @@ class FrontController { * @param ViewHttpResponse $response * @return void */ - private function displayViewByKind(ViewHttpResponse $response): void { + private function displayViewByKind(ViewHttpResponse $response): void + { $file = $response->getFile(); $args = $response->getArguments(); @@ -151,7 +158,7 @@ class FrontController { $loader = new FilesystemLoader('../src/Views/'); $twig = new Environment($loader); $twig->display($file, $args); - } catch (RuntimeError|SyntaxError|LoaderError $e) { + } catch (RuntimeError | SyntaxError | LoaderError $e) { http_response_code(500); echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); throw $e; diff --git a/src/Controller/Sub/EditorController.php b/src/Controller/Sub/EditorController.php index def6ac9..ce7a0a3 100644 --- a/src/Controller/Sub/EditorController.php +++ b/src/Controller/Sub/EditorController.php @@ -14,8 +14,8 @@ use App\Model\TacticModel; class EditorController { private TacticModel $model; - public function __construct() { - $this->model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); + public function __construct(TacticModel $model) { + $this->model = $model; } private function openEditor(TacticInfo $tactic): HttpResponse { diff --git a/src/Controller/TeamController.php b/src/Controller/Sub/TeamController.php similarity index 98% rename from src/Controller/TeamController.php rename to src/Controller/Sub/TeamController.php index 08e22cc..c3991a3 100644 --- a/src/Controller/TeamController.php +++ b/src/Controller/Sub/TeamController.php @@ -1,6 +1,6 @@ createNew(); } + + public function createTeam(): HttpResponse { + $model = new TeamModel(new TeamGateway(new Connexion(get_database()))); + $ctrl = new Sub\TeamController($model); + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + return $ctrl->displaySubmitTeam(); + } + return $ctrl->submitTeam($_POST); + } + + public function listTeams(): HttpResponse { + $model = new TeamModel(new TeamGateway(new Connexion(get_database()))); + $ctrl = new Sub\TeamController($model); + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + return $ctrl->displayListTeamByName(); + } + return $ctrl->listTeamByName($_POST); + } + + public function getTeam(int $id): HttpResponse { + $model = new TeamModel(new TeamGateway(new Connexion(get_database()))); + $ctrl = new Sub\TeamController($model); + return $ctrl->getTeam($id); + } } diff --git a/src/Views/display_teams.html.twig b/src/Views/display_teams.html.twig index c0ac185..47b3cbc 100644 --- a/src/Views/display_teams.html.twig +++ b/src/Views/display_teams.html.twig @@ -10,7 +10,7 @@

Aucune équipe n'a été trouvée

Chercher une équipe

-
+
@@ -22,7 +22,7 @@
{% else %} {% for t in teams %} -
+

Nom de l'équipe : {{ t.name }}

logo de l'équipe
diff --git a/src/Views/insert_team.html.twig b/src/Views/insert_team.html.twig index 0cc85dd..fbe6917 100644 --- a/src/Views/insert_team.html.twig +++ b/src/Views/insert_team.html.twig @@ -64,7 +64,7 @@

Créer une équipe

- +
diff --git a/src/Views/list_team_by_name.html.twig b/src/Views/list_team_by_name.html.twig index 1d9ddf3..1165da3 100644 --- a/src/Views/list_team_by_name.html.twig +++ b/src/Views/list_team_by_name.html.twig @@ -62,7 +62,7 @@

Chercher une équipe

- +
From ad4208e6dafca2573be166bcdcf852883481e98e Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 22 Nov 2023 13:46:08 +0100 Subject: [PATCH 52/75] fix format --- src/Controller/FrontController.php | 33 ++++++++++-------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index 7d61b28..eb796f7 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -14,12 +14,10 @@ use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Loader\FilesystemLoader; -class FrontController -{ +class FrontController { private AltoRouter $router; - public function __construct(string $basePath) - { + public function __construct(string $basePath) { $this->router = $this->createRouter($basePath); $this->initializeRouterMap(); } @@ -29,8 +27,7 @@ class FrontController * * @return void */ - public function run(): void - { + public function run(): void { $match = $this->router->match(); if ($match != null) { $this->handleMatch($match); @@ -45,8 +42,7 @@ class FrontController * @param string $basePath * @return AltoRouter */ - public function createRouter(string $basePath): AltoRouter - { + public function createRouter(string $basePath): AltoRouter { $router = new AltoRouter(); $router->setBasePath($basePath); return $router; @@ -57,8 +53,7 @@ class FrontController * * @return void */ - private function initializeRouterMap(): void - { + private function initializeRouterMap(): void { $this->router->map("GET", "/", "UserController"); $this->router->map("GET|POST", "/[a:action]?/[i:id]", "UserController"); $this->router->map("GET|POST", "/tactic/[a:action]/[i:idTactic]?", "UserController"); @@ -68,8 +63,7 @@ class FrontController * @param array $match * @return void */ - private function handleMatch(array $match): void - { + private function handleMatch(array $match): void { $tag = $match['target']; $action = $this->getAction($match); @@ -84,8 +78,7 @@ class FrontController * @param array $params * @return HttpResponse */ - private function tryToCall(string $controller, string $action, array $params): HttpResponse - { + private function tryToCall(string $controller, string $action, array $params): HttpResponse { $controller = $this->getController($controller); if (is_callable([$controller, $action])) { return call_user_func_array([$controller, $action], $params); @@ -100,8 +93,7 @@ class FrontController * @param array $match * @return string */ - private function getAction(array $match): string - { + private function getAction(array $match): string { if (isset($match["params"]["action"])) { return $match["params"]["action"]; } @@ -114,8 +106,7 @@ class FrontController * @param string $controller * @return mixed */ - private function getController(string $controller) - { + private function getController(string $controller) { $namespace = "\\App\\Controller\\"; $controller = $namespace . $controller; return new $controller(); @@ -127,8 +118,7 @@ class FrontController * @param HttpResponse $response * @return void */ - private function handleResponseByType(HttpResponse $response): void - { + private function handleResponseByType(HttpResponse $response): void { http_response_code($response->getCode()); if ($response instanceof ViewHttpResponse) { $this->displayViewByKind($response); @@ -144,8 +134,7 @@ class FrontController * @param ViewHttpResponse $response * @return void */ - private function displayViewByKind(ViewHttpResponse $response): void - { + private function displayViewByKind(ViewHttpResponse $response): void { $file = $response->getFile(); $args = $response->getArguments(); From d54559e01d3d734d87c95607d2dfbc5a2a616695 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 22 Nov 2023 14:43:03 +0100 Subject: [PATCH 53/75] fix --- src/Views/insert_team.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Views/insert_team.html.twig b/src/Views/insert_team.html.twig index fbe6917..0fc59ca 100644 --- a/src/Views/insert_team.html.twig +++ b/src/Views/insert_team.html.twig @@ -64,7 +64,7 @@

Créer une équipe

- +
From 28171b35e0e9bdb548b031040054b51f5699083c Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Wed, 22 Nov 2023 14:43:29 +0100 Subject: [PATCH 54/75] Adaptation welcome page --- src/Views/home.twig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Views/home.twig b/src/Views/home.twig index 1c14e02..1429363 100644 --- a/src/Views/home.twig +++ b/src/Views/home.twig @@ -73,13 +73,13 @@

Mes strategies

- + {% if recentTactic != null %} {% for tactic in recentTactic %} -
+

{{tactic.id}} - {{tactic.name}} - {{tactic.creation_date}}

- +
{% endfor %} {% else %} From 19d2ed01993723749e2a13f27710a4ee816e378e Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Wed, 22 Nov 2023 14:52:33 +0100 Subject: [PATCH 55/75] Merging in progress --- src/Controller/UserController.php | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index c5f9ef2..3bc3d71 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -3,15 +3,18 @@ namespace App\Controller; use App\Connexion; -<<<<<<< HEAD use App\Gateway\AuthGateway; use App\Gateway\TacticInfoGateway; use App\Http\HttpResponse; use App\Http\ViewHttpResponse; use App\Model\AuthModel; use App\Model\TacticModel; +use App\Gateway\TeamGateway; +use App\Model\TeamModel; +use App\Session\SessionHandle; +use App\Controller\Sub\EditorController; -class UserController { +class UserController extends VisitorController { private TacticModel $tacticMdl; public function __construct() @@ -20,18 +23,6 @@ class UserController { $this->tacticMdl = new TacticModel(new TacticInfoGateway($con)); } -======= -use App\Gateway\TacticInfoGateway; -use App\Gateway\TeamGateway; -use App\Http\HttpResponse; -use App\Http\ViewHttpResponse; -use App\Model\TacticModel; -use App\Model\TeamModel; -use App\Session\SessionHandle; - -class UserController extends VisitorController { - ->>>>>>> d54559e01d3d734d87c95607d2dfbc5a2a616695 public function home(): HttpResponse { $listTactic = $this->tacticMdl->getLast(5); return ViewHttpResponse::twig("home.twig", ["recentTactic" => $listTactic]); @@ -44,12 +35,12 @@ class UserController extends VisitorController { public function edit(int $id, SessionHandle $session): HttpResponse { $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); - return (new Sub\EditorController($model))->edit($id, $session); + return (new EditorController($model))->edit($id, $session); } public function create(SessionHandle $session): HttpResponse { $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); - return (new Sub\EditorController($model))->createNew($session); + return (new EditorController($model))->createNew($session); } public function open(int $id, SessionHandle $session): HttpResponse { From 83a19fd93d0c5110e9bf2c43eb1c3028e7126243 Mon Sep 17 00:00:00 2001 From: samuel Date: Wed, 22 Nov 2023 12:58:03 +0100 Subject: [PATCH 56/75] add error messages --- sql/setup-tables.sql | 7 ------- src/Controller/Sub/AuthController.php | 14 ++++---------- src/Controller/UserController.php | 21 ++------------------- src/Model/AuthModel.php | 8 ++++---- src/Validation/Validators.php | 4 ++-- src/Views/display_login.html.twig | 18 ++++++++++++------ src/Views/display_register.html.twig | 14 ++++++++++++-- 7 files changed, 36 insertions(+), 50 deletions(-) diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index e105a1e..abf5049 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -44,10 +44,3 @@ CREATE TABLE Member FOREIGN KEY (idTeam) REFERENCES Team (id), FOREIGN KEY (idMember) REFERENCES User (id) ); - -CREATE TABLE TacticInfo -( - id integer PRIMARY KEY AUTOINCREMENT, - name varchar, - creation_date timestamp DEFAULT CURRENT_TIMESTAMP -); diff --git a/src/Controller/Sub/AuthController.php b/src/Controller/Sub/AuthController.php index d94da43..709be3e 100644 --- a/src/Controller/Sub/AuthController.php +++ b/src/Controller/Sub/AuthController.php @@ -30,14 +30,8 @@ class AuthController { * @param ValidationFail[] $fails * @return HttpResponse */ - private function displayBadFields(string $viewName, array $fails): HttpResponse { - $bad_fields = []; - foreach ($fails as $err) { - if ($err instanceof FieldValidationFail) { - $bad_fields[] = $err->getFieldName(); - } - } - return ViewHttpResponse::twig($viewName, ['bad_fields' => $bad_fields]); + private function displayBadFields(string $viewName, array $fails): HttpResponse{ + return ViewHttpResponse::twig($viewName, ['fails' => $fails]); } /** @@ -51,7 +45,7 @@ class AuthController { "username" => [Validators::name(), Validators::lenBetween(2, 32)], "password" => [Validators::lenBetween(6, 256)], "confirmpassword" => [Validators::lenBetween(6, 256)], - "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/","invalide"),Validators::lenBetween(5, 256)], ]); if (!empty($fails)) { return $this->displayBadFields("display_register.html.twig", $fails); @@ -80,7 +74,7 @@ class AuthController { $fails = []; $request = HttpRequest::from($request, $fails, [ "password" => [Validators::lenBetween(6, 256)], - "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/","invalide"),Validators::lenBetween(5, 256)], ]); if (!empty($fails)) { return $this->displayBadFields("display_login.html.twig", $fails); diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index c5f9ef2..58cc2a8 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -3,24 +3,7 @@ namespace App\Controller; use App\Connexion; -<<<<<<< HEAD -use App\Gateway\AuthGateway; -use App\Gateway\TacticInfoGateway; -use App\Http\HttpResponse; -use App\Http\ViewHttpResponse; -use App\Model\AuthModel; -use App\Model\TacticModel; -class UserController { - private TacticModel $tacticMdl; - - public function __construct() - { - $con = new Connexion(get_database()); - $this->tacticMdl = new TacticModel(new TacticInfoGateway($con)); - } - -======= use App\Gateway\TacticInfoGateway; use App\Gateway\TeamGateway; use App\Http\HttpResponse; @@ -31,9 +14,9 @@ use App\Session\SessionHandle; class UserController extends VisitorController { ->>>>>>> d54559e01d3d734d87c95607d2dfbc5a2a616695 public function home(): HttpResponse { - $listTactic = $this->tacticMdl->getLast(5); + $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); + $listTactic = $model->getLast(5); return ViewHttpResponse::twig("home.twig", ["recentTactic" => $listTactic]); } diff --git a/src/Model/AuthModel.php b/src/Model/AuthModel.php index 1739ab5..bd359ba 100644 --- a/src/Model/AuthModel.php +++ b/src/Model/AuthModel.php @@ -29,11 +29,11 @@ class AuthModel { public function register(string $username, string $password, string $confirmPassword, string $email, array &$failures): ?Account { if ($password != $confirmPassword) { - $failures[] = new FieldValidationFail("confirmpassword", "password and password confirmation are not equals"); + $failures[] = new FieldValidationFail("confirmpassword", "Le mot de passe et la confirmation ne sont pas les mêmes."); } if ($this->gateway->exists($email)) { - $failures[] = new FieldValidationFail("email", "email already exist"); + $failures[] = new FieldValidationFail("email", "L'email existe déjà"); } if (!empty($failures)) { @@ -59,13 +59,13 @@ class AuthModel { */ public function login(string $email, string $password, array &$failures): ?Account { if (!$this->gateway->exists($email)) { - $failures[] = new FieldValidationFail("email", "email doesnt exists"); + $failures[] = new FieldValidationFail("email", "Vous n'êtes pas enregistré."); return null; } $hash = $this->gateway->getHash($email); if (!password_verify($password, $hash)) { - $failures[] = new FieldValidationFail("password", "invalid password"); + $failures[] = new FieldValidationFail("password", "Mot de passe invalide."); return null; } diff --git a/src/Validation/Validators.php b/src/Validation/Validators.php index cb28007..6b72ff0 100644 --- a/src/Validation/Validators.php +++ b/src/Validation/Validators.php @@ -41,10 +41,10 @@ class Validators { function (string $fieldName, string $str) use ($min, $max) { $len = strlen($str); if ($len >= $max) { - return [new FieldValidationFail($fieldName, "field is longer than $max chars.")]; + return [new FieldValidationFail($fieldName, "trop long, maximum $max caractères.")]; } if ($len < $min) { - return [new FieldValidationFail($fieldName, "field is shorted than $min chars.")]; + return [new FieldValidationFail($fieldName, "trop court, minimum $min caractères.")]; } return []; } diff --git a/src/Views/display_login.html.twig b/src/Views/display_login.html.twig index 33b2385..ca6890d 100644 --- a/src/Views/display_login.html.twig +++ b/src/Views/display_login.html.twig @@ -53,26 +53,32 @@ background-color: #0056b3; } - {% for err in bad_fields %} - .form-group #{{ err }} { + .error-messages{ + color : #ff331a; + font-style: italic; + } + + {% for err in fails %} + .form-group #{{ err.getFieldName() }} { border-color: red; } {% endfor %} - - -

Se connecter

+ + {% for name in fails %} + + {% endfor %} + -
diff --git a/src/Views/display_register.html.twig b/src/Views/display_register.html.twig index 40199a0..2b24e23 100644 --- a/src/Views/display_register.html.twig +++ b/src/Views/display_register.html.twig @@ -49,12 +49,17 @@ cursor: pointer; } + .error-messages{ + color : #ff331a; + font-style: italic; + } + input[type="submit"]:hover { background-color: #0056b3; } - {% for err in bad_fields %} - .form-group #{{ err }} { + {% for err in fails %} + .form-group #{{ err.getFieldName() }} { border-color: red; } {% endfor %} @@ -67,6 +72,11 @@

S'enregistrer

+ + {% for name in fails %} + + {% endfor %} + From a0208115183bbda3152408a2fa75cfc33e239902 Mon Sep 17 00:00:00 2001 From: Vivien DUFOUR Date: Wed, 22 Nov 2023 15:19:16 +0100 Subject: [PATCH 57/75] team/addMembers (#21) Co-authored-by: vivien.dufour Reviewed-on: https://codefirst.iut.uca.fr/git/IQBall/Application-Web/pulls/21 --- public/index.php | 1 + sql/setup-tables.sql | 12 ++- src/Controller/Sub/TeamController.php | 36 ++++++++- src/Controller/UserController.php | 19 +++++ src/Data/Account.php | 2 - src/Data/Member.php | 18 ++++- src/Data/Team.php | 4 + src/Gateway/AuthGateway.php | 47 ++++++++++++ src/Gateway/TeamGateway.php | 42 +++++++++-- src/Model/TeamModel.php | 23 ++++-- src/Views/add_member.html.twig | 103 ++++++++++++++++++++++++++ src/Views/delete_member.html.twig | 73 ++++++++++++++++++ src/Views/display_team.html.twig | 11 ++- src/Views/display_teams.html.twig | 4 +- src/Views/list_team_by_name.html.twig | 2 +- 15 files changed, 365 insertions(+), 32 deletions(-) create mode 100644 src/Gateway/AuthGateway.php create mode 100644 src/Views/add_member.html.twig create mode 100644 src/Views/delete_member.html.twig diff --git a/public/index.php b/public/index.php index cd4868d..d8dd098 100644 --- a/public/index.php +++ b/public/index.php @@ -11,6 +11,7 @@ use App\Controller\FrontController; use App\Session\PhpSessionHandle; $basePath = get_public_path(); + $frontController = new FrontController($basePath); $frontController->run(PhpSessionHandle::init()); diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index abf5049..b13712f 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -22,6 +22,9 @@ CREATE TABLE Tactic FOREIGN KEY (owner) REFERENCES Account ); +CREATE TABLE FormEntries(name varchar, description varchar); + + CREATE TABLE Team ( id integer PRIMARY KEY AUTOINCREMENT, @@ -31,14 +34,9 @@ CREATE TABLE Team secondColor varchar ); -CREATE TABLE User -( - id integer PRIMARY KEY AUTOINCREMENT -); -CREATE TABLE Member -( - idTeam integer, +CREATE TABLE Member( + idTeam integer, idMember integer, role char(1) CHECK (role IN ('C', 'P')), FOREIGN KEY (idTeam) REFERENCES Team (id), diff --git a/src/Controller/Sub/TeamController.php b/src/Controller/Sub/TeamController.php index c3991a3..adefa41 100644 --- a/src/Controller/Sub/TeamController.php +++ b/src/Controller/Sub/TeamController.php @@ -23,11 +23,17 @@ class TeamController { return ViewHttpResponse::twig("insert_team.html.twig", []); } - /** - * @param array $request - * @return HttpResponse - */ + + public function displayAddMember() : HttpResponse { + return ViewHttpResponse::twig("add_member.html.twig", []); + } + + public function displayDeleteMember() : HttpResponse { + return ViewHttpResponse::twig("delete_member.html.twig", []); + } + public function submitTeam(array $request): HttpResponse { + $errors = []; $request = HttpRequest::from($request, $errors, [ @@ -80,4 +86,26 @@ class TeamController { $result = $this->model->displayTeam($id); return ViewHttpResponse::twig('display_team.html.twig', ['team' => $result]); } + + public function addMember(array $request) : HttpResponse { + $errors = []; + + $request = HttpRequest::from($request, $errors, [ + "team" => [Validators::isInteger()], + "mail" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)] + ]); + + return $this->getTeam($this->model->addMember($request['mail'], intval($request['team']), $request['role'])); + } + + public function deleteMember(array $request) : HttpResponse { + $errors = []; + + $request = HttpRequest::from($request, $errors, [ + "team" => [Validators::isInteger()], + "mail" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)] + ]); + + return $this->getTeam($this->model->deleteMember($request['mail'], intval($request['team']))); + } } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 674f81e..46f4992 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -62,4 +62,23 @@ class UserController extends VisitorController { $ctrl = new Sub\TeamController($model); return $ctrl->getTeam($id); } + + public function addMember(): HttpResponse { + $model = new TeamModel(new TeamGateway(new Connexion(get_database()))); + $ctrl = new Sub\TeamController($model); + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + return $ctrl->displayAddMember($_POST); + } + return $ctrl->addMember($_POST); + } + + public function deleteMember(): HttpResponse { + $model = new TeamModel(new TeamGateway(new Connexion(get_database()))); + $ctrl = new Sub\TeamController($model); + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + return $ctrl->displayDeleteMember($_POST); + } + return $ctrl->deleteMember($_POST); + } + } diff --git a/src/Data/Account.php b/src/Data/Account.php index 0ed4339..6a61563 100755 --- a/src/Data/Account.php +++ b/src/Data/Account.php @@ -2,8 +2,6 @@ namespace App\Data; -use http\Exception\InvalidArgumentException; - /** * Base class of a user account. * Contains the private information that we don't want diff --git a/src/Data/Member.php b/src/Data/Member.php index b415f65..3049fde 100755 --- a/src/Data/Member.php +++ b/src/Data/Member.php @@ -7,10 +7,15 @@ namespace App\Data; */ class Member { /** - * @var int The member's user id + * @var AccountUser The member's user account */ private int $userId; + /** + * @var int The member's team id + */ + private int $teamId; + /** * @var MemberRole the member's role */ @@ -20,8 +25,9 @@ class Member { * @param int $userId * @param MemberRole $role */ - public function __construct(int $userId, MemberRole $role) { + public function __construct(int $userId, int $teamId, MemberRole $role) { $this->userId = $userId; + $this->teamId = $teamId; $this->role = $role; } @@ -39,4 +45,12 @@ class Member { public function getRole(): MemberRole { return $this->role; } + + /** + * @return int + */ + public function getTeamId(): int + { + return $this->teamId; + } } diff --git a/src/Data/Team.php b/src/Data/Team.php index e50ad40..a359722 100755 --- a/src/Data/Team.php +++ b/src/Data/Team.php @@ -72,4 +72,8 @@ class Team { return $this->members; } + public function addMember(Member $m) { + $this->members[] = $m; + } + } diff --git a/src/Gateway/AuthGateway.php b/src/Gateway/AuthGateway.php new file mode 100644 index 0000000..3370d26 --- /dev/null +++ b/src/Gateway/AuthGateway.php @@ -0,0 +1,47 @@ +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|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; + } + + + + +} diff --git a/src/Gateway/TeamGateway.php b/src/Gateway/TeamGateway.php index c3d22dc..c98d803 100644 --- a/src/Gateway/TeamGateway.php +++ b/src/Gateway/TeamGateway.php @@ -24,10 +24,19 @@ class TeamGateway { ); } - /** - * @param string $name - * @return array[] - */ + + public function insertMember(int $idTeam, int $idMember, string $role) { + $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] + ] + ); + } + + public function listByName(string $name): array { return $this->con->fetch( "SELECT id,name,picture,mainColor,secondColor FROM Team WHERE name LIKE '%' || :name || '%'", @@ -69,11 +78,30 @@ class TeamGateway { */ public function getMembersById(int $id): array { return $this->con->fetch( - "SELECT m.role,u.id FROM User u,Team t,Member m WHERE t.id = :id AND m.idTeam = t.id AND m.idMember = u.id", - [ - ":id" => [$id, PDO::PARAM_INT], + "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] ] ); } + public function getMemberIdByMail($mail) : array { + return $this->con->fetch( + "SELECT id FROM Account WHERE email = :mail", + [ + ":mail" => [$mail, PDO::PARAM_STR] + ] + ); + } + + public function deleteMember(int $idTeam, int $idMember) { + $this->con->exec( + "DELETE FROM Member WHERE idTeam = :idTeam AND idMember = :idMember", + [ + ":idTeam" => [$idTeam, PDO::PARAM_INT], + ":idMember" => [$idMember, PDO::PARAM_INT], + ] + ); + } + } diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php index 65f22a7..c936d30 100644 --- a/src/Model/TeamModel.php +++ b/src/Model/TeamModel.php @@ -24,10 +24,14 @@ class TeamModel { return intval($result[0]['id']); } - /** - * @param string $name - * @return Team[] - */ + + public function addMember(string $mail, int $teamId, string $role) : int { + $result = $this->gateway->getMemberIdByMail($mail)[0]; + $memberId = intval($result['id']); + $this->gateway->insertMember($teamId, $memberId, $role); + return $teamId; + } + public function listByName(string $name): array { $teams = []; $results = $this->gateway->listByName($name); @@ -42,13 +46,22 @@ class TeamModel { $result = $this->gateway->getTeamById($id)[0]; $resultMembers = $this->gateway->getMembersById($id); foreach ($resultMembers as $row) { + var_dump($row['role']); if ($row['role'] == 'C') { $role = MemberRole::coach(); } else { $role = MemberRole::player(); } - $members[] = new Member($row['id'], $role); + $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); } + + public function deleteMember(string $mail, int $teamId) : int { + $result = $this->gateway->getMemberIdByMail($mail)[0]; + $memberId = intval($result['id']); + $this->gateway->deleteMember($teamId, $memberId); + return $teamId; + } + } diff --git a/src/Views/add_member.html.twig b/src/Views/add_member.html.twig new file mode 100644 index 0000000..ef74da2 --- /dev/null +++ b/src/Views/add_member.html.twig @@ -0,0 +1,103 @@ + + + + + Ajouter un membre + + + + +
+

Ajouter un membre à votre équipe

+ +
+ + + + + +
+ Rôle du membre dans l'équipe : +
+ + +
+
+ + +
+
+ +
+
+ +
+ +
+ + + \ No newline at end of file diff --git a/src/Views/delete_member.html.twig b/src/Views/delete_member.html.twig new file mode 100644 index 0000000..a478284 --- /dev/null +++ b/src/Views/delete_member.html.twig @@ -0,0 +1,73 @@ + + + + + Ajouter un membre + + + + +
+

Supprimez un membre de votre équipe

+
+
+ + + + +
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/src/Views/display_team.html.twig b/src/Views/display_team.html.twig index ada8566..474397e 100644 --- a/src/Views/display_team.html.twig +++ b/src/Views/display_team.html.twig @@ -54,6 +54,7 @@ height: 80px; width: 80px; } + @@ -72,8 +73,14 @@

Couleur principale :

Couleur secondaire :

- {% for m in team.members %} -

m.id

+ + {% for m in team.listMembers() %} +

{{ m.getUserId() }}

+ {% if m.getRole().isCoach() %} +

: Coach

+ {% else %} +

: Joueur

+ {% endif %} {% endfor %}
diff --git a/src/Views/display_teams.html.twig b/src/Views/display_teams.html.twig index 47b3cbc..bf89909 100644 --- a/src/Views/display_teams.html.twig +++ b/src/Views/display_teams.html.twig @@ -10,7 +10,7 @@

Aucune équipe n'a été trouvée

Chercher une équipe

-
+
@@ -22,7 +22,7 @@
{% else %} {% for t in teams %} -
+

Nom de l'équipe : {{ t.name }}

logo de l'équipe
diff --git a/src/Views/list_team_by_name.html.twig b/src/Views/list_team_by_name.html.twig index 1165da3..0ea1905 100644 --- a/src/Views/list_team_by_name.html.twig +++ b/src/Views/list_team_by_name.html.twig @@ -62,7 +62,7 @@

Chercher une équipe

- +
From 34777e70df3d900675f8b90f1b0f2751240cdc5e Mon Sep 17 00:00:00 2001 From: DahmaneYanis Date: Wed, 22 Nov 2023 15:27:46 +0100 Subject: [PATCH 58/75] Add settings --- src/Controller/UserController.php | 4 ++++ src/Views/account_settings.twig | 24 ++++++++++++++++++++++++ src/Views/home.twig | 12 +++++++----- 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 src/Views/account_settings.twig diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 674f81e..428e20f 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -19,6 +19,10 @@ class UserController extends VisitorController { return ViewHttpResponse::twig("home.twig", ["recentTactic" => $listTactic]); } + public function settings(): HttpResponse { + return ViewHttpResponse::twig("account_settings.twig", []); + } + public function view(int $id, SessionHandle $session): HttpResponse { $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); return (new Sub\VisualizerController($model))->visualize($id, $session); diff --git a/src/Views/account_settings.twig b/src/Views/account_settings.twig new file mode 100644 index 0000000..3d8ead3 --- /dev/null +++ b/src/Views/account_settings.twig @@ -0,0 +1,24 @@ + + + + + + + Paramètres + + + + + + + + +

Paramètres

+ + \ No newline at end of file diff --git a/src/Views/home.twig b/src/Views/home.twig index 1429363..6912c89 100644 --- a/src/Views/home.twig +++ b/src/Views/home.twig @@ -6,6 +6,7 @@ content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> Page d'accueil + +

IQ Ball

-
+
Account logo Date: Wed, 22 Nov 2023 15:35:23 +0100 Subject: [PATCH 59/75] Add button --- src/Views/home.twig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Views/home.twig b/src/Views/home.twig index 6912c89..35379ce 100644 --- a/src/Views/home.twig +++ b/src/Views/home.twig @@ -63,6 +63,8 @@

Mes équipes

+ + {% if recentTeam != null %} {% for team in recentTeam %}
From 36051ebd830d81c0a67c0d9f05f5ed67b468aa43 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Fri, 24 Nov 2023 01:59:08 +0100 Subject: [PATCH 60/75] good routes, good controllers --- .../assets/icon}/account.png | Bin .../assets/icon}/account.svg | 0 public/api/index.php | 31 +++--- public/index.php | 87 ++++++++++++++++- src/Controller/{Sub => }/AuthController.php | 3 +- src/Controller/{Sub => }/EditorController.php | 7 +- src/Controller/Route/Action.php | 71 ++++++++++++++ .../{ => Route}/FrontController.php | 80 +++------------- src/Controller/{Sub => }/TeamController.php | 31 +++--- src/Controller/UserController.php | 90 ++++-------------- src/Controller/VisitorController.php | 28 ------ .../{Sub => }/VisualizerController.php | 5 +- src/Data/Color.php | 3 - src/Views/add_member.html.twig | 2 +- src/Views/delete_member.html.twig | 2 +- src/Views/display_teams.html.twig | 4 +- src/Views/home.twig | 12 +-- src/Views/insert_team.html.twig | 2 +- src/Views/list_team_by_name.html.twig | 2 +- 19 files changed, 238 insertions(+), 222 deletions(-) rename {public/img/welcomePage => front/assets/icon}/account.png (100%) rename {public/img/welcomePage => front/assets/icon}/account.svg (100%) rename src/Controller/{Sub => }/AuthController.php (97%) rename src/Controller/{Sub => }/EditorController.php (87%) create mode 100644 src/Controller/Route/Action.php rename src/Controller/{ => Route}/FrontController.php (61%) rename src/Controller/{Sub => }/TeamController.php (68%) delete mode 100644 src/Controller/VisitorController.php rename src/Controller/{Sub => }/VisualizerController.php (90%) diff --git a/public/img/welcomePage/account.png b/front/assets/icon/account.png similarity index 100% rename from public/img/welcomePage/account.png rename to front/assets/icon/account.png diff --git a/public/img/welcomePage/account.svg b/front/assets/icon/account.svg similarity index 100% rename from public/img/welcomePage/account.svg rename to front/assets/icon/account.svg diff --git a/public/api/index.php b/public/api/index.php index c5f8f2e..cb278df 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -30,7 +30,8 @@ function getAuthController(): APIAuthController { /** * A Front controller action */ -class Action { +//TODO workaround for generic Action +class ApiAction { /** * @var callable(mixed[]): HttpResponse $action action to call */ @@ -69,18 +70,18 @@ class Action { /** * @param callable(mixed[]): HttpResponse $action - * @return Action an action that does not require to have an authorization. + * @return ApiAction an action that does not require to have an authorization. */ - public static function noAuth(callable $action): Action { - return new Action($action, false); + public static function noAuth(callable $action): ApiAction { + return new ApiAction($action, false); } /** * @param callable(mixed[]): HttpResponse $action - * @return Action an action that does require to have an authorization. + * @return ApiAction an action that does require to have an authorization. */ - public static function auth(callable $action): Action { - return new Action($action, true); + public static function auth(callable $action): ApiAction { + return new ApiAction($action, true); } } @@ -95,23 +96,23 @@ function handleMatch(array $match): HttpResponse { } $action = $match['target']; - if (!$action instanceof Action) { + if (!$action instanceof ApiAction) { throw new Exception("routed action is not an Action object."); } $auth = null; if ($action->isAuthRequired()) { - $auth = tryGetAuthAccount(); + $auth = tryGetAuthorization(); if ($auth == null) { - return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header")]); + return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")]); } } return $action->run($match['params'], $auth); } -function tryGetAuthAccount(): ?Account { +function tryGetAuthorization(): ?Account { $headers = getallheaders(); // If no authorization header is set, try fallback to php session. @@ -128,10 +129,10 @@ function tryGetAuthAccount(): ?Account { $router = new AltoRouter(); $router->setBasePath(get_public_path() . "/api"); -$router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); -$router->map("GET", "/tactic/[i:id]", Action::auth(fn(int $id, Account $acc) => getTacticController()->getTacticInfo($id, $acc))); -$router->map("POST", "/tactic/new", Action::auth(fn(Account $acc) => getTacticController()->newTactic($acc))); -$router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize())); +$router->map("POST", "/tactic/[i:id]/edit/name", ApiAction::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); +$router->map("GET", "/tactic/[i:id]", ApiAction::auth(fn(int $id, Account $acc) => getTacticController()->getTacticInfo($id, $acc))); +$router->map("POST", "/tactic/new", ApiAction::auth(fn(Account $acc) => getTacticController()->newTactic($acc))); +$router->map("POST", "/auth", ApiAction::noAuth(fn() => getAuthController()->authorize())); $match = $router->match(); diff --git a/public/index.php b/public/index.php index d8dd098..8d1f148 100644 --- a/public/index.php +++ b/public/index.php @@ -7,12 +7,93 @@ require "../sql/database.php"; require "utils.php"; require "../src/react-display.php"; -use App\Controller\FrontController; +use App\Controller\AuthController; +use App\Controller\EditorController; +use App\Controller\Route\Action; +use App\Controller\Route\FrontController; +use App\Controller\TeamController; +use App\Controller\UserController; +use App\Controller\VisualizerController; +use App\Gateway\AccountGateway; +use App\Gateway\TacticInfoGateway; +use App\Gateway\TeamGateway; +use App\Model\AuthModel; +use App\Model\TacticModel; +use App\Model\TeamModel; +use App\Session\MutableSessionHandle; use App\Session\PhpSessionHandle; +use App\Connexion; +use App\Session\SessionHandle; +$connexion = new Connexion(get_database()); + +function getUserController(): UserController { + global $connexion; + return new UserController(new TacticModel(new TacticInfoGateway($connexion))); +} + +function getVisualizerController(): VisualizerController { + global $connexion; + return new VisualizerController(new TacticModel(new TacticInfoGateway($connexion))); +} + +function getEditorController(): EditorController { + global $connexion; + return new EditorController(new TacticModel(new TacticInfoGateway($connexion))); +} + +function getTeamController(): TeamController { + global $connexion; + return new TeamController(new TeamModel(new TeamGateway($connexion))); +} + +function getAuthController(): AuthController { + global $connexion; + return new AuthController(new AuthModel(new AccountGateway($connexion))); +} + +function initFrontController(FrontController $fc) { + //authentication + $fc->addRoute("GET", "/login", Action::noAuth(fn() => getAuthController()->displayLogin())); + $fc->addRoute("GET", "/register", Action::noAuth(fn() => getAuthController()->displayRegister())); + $fc->addRoute("POST", "/login", Action::noAuth(fn(SessionHandle $s) => getAuthController()->confirmLogin($_POST, $s))); + $fc->addRoute("POST", "/register", Action::noAuth(fn(SessionHandle $s) => getAuthController()->confirmRegister($_POST, $s))); + + //user-related + $fc->addRoute("GET", "/home", Action::auth(fn(SessionHandle $s) => getUserController()->home($s))); + $fc->addRoute("GET", "/settings", Action::auth(fn(SessionHandle $s) => getUserController()->settings($s))); + + //tactic-related + $fc->addRoute("GET", "/tactic/[i:id]/view", Action::auth(fn(int $id, SessionHandle $s) => getVisualizerController()->visualize($id, $s))); + $fc->addRoute("GET", "/tactic/[i:id]/edit", Action::auth(fn(int $id, SessionHandle $s) => getEditorController()->edit($id, $s))); + $fc->addRoute("GET", "/tactic/new", Action::auth(fn(SessionHandle $s) => getEditorController()->createNew($s))); + + //team-related + $fc->addRoute("GET", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->displayCreateTeam($s))); + $fc->addRoute("POST", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->submitTeam($_POST, $s))); + $fc->addRoute("GET", "/team/search", Action::auth(fn(SessionHandle $s) => getTeamController()->displayListTeamByName($s))); + $fc->addRoute("POST", "/team/search", Action::auth(fn(SessionHandle $s) => getTeamController()->listTeamByName($_POST, $s))); + $fc->addRoute("GET", "/team/[i:id]", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->displayTeam($id, $s))); + $fc->addRoute("GET", "/team/members/add", Action::auth(fn(SessionHandle $s) => getTeamController()->displayAddMember($s))); + $fc->addRoute("POST", "/team/members/add", Action::auth(fn(SessionHandle $s) => getTeamController()->addMember($_POST, $s))); + $fc->addRoute("GET", "/team/members/remove", Action::auth(fn(SessionHandle $s) => getTeamController()->displayDeleteMember($s))); + $fc->addRoute("POST", "/team/members/remove", Action::auth(fn(SessionHandle $s) => getTeamController()->deleteMember($_POST, $s))); +} + +//this is a global variable $basePath = get_public_path(); -$frontController = new FrontController($basePath); -$frontController->run(PhpSessionHandle::init()); +function run() { + global $basePath; + + $fc = new FrontController($basePath); + + initFrontController($fc); + $fc->run(PhpSessionHandle::init()); +} + + +run(); + diff --git a/src/Controller/Sub/AuthController.php b/src/Controller/AuthController.php similarity index 97% rename from src/Controller/Sub/AuthController.php rename to src/Controller/AuthController.php index 709be3e..0ee9a46 100644 --- a/src/Controller/Sub/AuthController.php +++ b/src/Controller/AuthController.php @@ -1,13 +1,12 @@ action = $action; + $this->isAuthRequired = $isAuthRequired; + } + + public function isAuthRequired(): bool { + return $this->isAuthRequired; + } + + /** + * @param mixed[] $params + * @param SessionHandle $session + * @return HttpResponse + * @throws Exception

+ * thrown if this action is required to be authenticated, but the given session does not contain a logged-in account. + *

+ *

+ * Caller is supposed to ensure that the user is logged-in before, if `$this->isAuthRequired()` is true before + * running this action. + *

+ */ + public function run(array $params, SessionHandle $session): HttpResponse { + $params = array_values($params); + if ($this->isAuthRequired) { + if ($session->getAccount() == null) { + throw new Exception("action requires authorization."); + } + } + + $params[] = $session; + return call_user_func_array($this->action, $params); + } + + /** + * @param callable(mixed[]): HttpResponse $action + * @return Action an action that does not require to have an authorization. + */ + public static function noAuth(callable $action): Action { + return new Action($action, false); + } + + /** + * @param callable(mixed[]): HttpResponse $action + * @return Action an action that does require to have an authorization. + */ + public static function auth(callable $action): Action { + return new Action($action, true); + } +} \ No newline at end of file diff --git a/src/Controller/FrontController.php b/src/Controller/Route/FrontController.php similarity index 61% rename from src/Controller/FrontController.php rename to src/Controller/Route/FrontController.php index d02bcbc..05b440e 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/Route/FrontController.php @@ -1,6 +1,6 @@ router = $this->createRouter($basePath); - $this->initializeRouterMap(); $this->basePath = $basePath; } + public function addRoute(string $method, string $path, Action $action): void { + $this->router->map($method, $path, $action); + } + /** * @param MutableSessionHandle $session * @return void @@ -61,85 +60,36 @@ class FrontController { return $router; } - /** - * Initialize project's routes - * - * @return void - */ - private function initializeRouterMap(): void { - $this->router->map("GET", "/home", self::USER_CONTROLLER); - $this->router->map("GET|POST", "/user/[a:action]/[i:idTactic]?", self::USER_CONTROLLER); - $this->router->map("GET|POST", "/visitor/[a:action]", self::VISITOR_CONTROLLER); - } - /** * @param array $match * @param MutableSessionHandle $session * @return void */ private function handleMatch(array $match, MutableSessionHandle $session): void { - $tag = $match['target']; - - $action = $this->getAction($match); - $params = $match["params"]; - unset($params['action']); - $this->handleResponseByType($this->tryToCall($tag, $action, array_values($params), $session)); + $action = $match['target']; + $params = array_values($match["params"]); + $this->handleResponseByType($this->tryToCall($action, $params, $session)); } /** - * @param string $controllerName - * @param string $action + * @param Action $action * @param array $params * @param MutableSessionHandle $session * @return HttpResponse */ - private function tryToCall(string $controllerName, string $action, array $params, MutableSessionHandle $session): HttpResponse { - if ($controllerName != self::VISITOR_CONTROLLER) { + private function tryToCall(Action $action, array $params, MutableSessionHandle $session): HttpResponse { + $account = null; + if ($action->isAuthRequired()) { $account = $session->getAccount(); if ($account == null) { // put in the session the initial url the user wanted to get $session->setInitialTarget($_SERVER['REQUEST_URI']); - return HttpResponse::redirect($this->basePath . "/visitor/login"); + return HttpResponse::redirect($this->basePath . "/login"); } } - $controller = $this->getController($controllerName); - - if (is_callable([$controller, $action])) { - // append the session as the last parameter of a controller function - $params[] = $session; - return call_user_func_array([$controller, $action], $params); - } else { - return ViewHttpResponse::twig("error.html.twig", [ - 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")], - ], HttpCodes::NOT_FOUND); - } - } - - /** - * Get the right method to call to do an action - * - * @param array $match - * @return string - */ - private function getAction(array $match): string { - if (isset($match["params"]["action"])) { - return $match["params"]["action"]; - } - return "home"; - } - - /** - * Initialize the right controller by the user's role - * - * @param string $controller - * @return mixed - */ - private function getController(string $controller) { - $namespace = "\\App\\Controller\\"; - $controller = $namespace . $controller; - return new $controller(); + return $action->run($params, $session); } /** @@ -182,7 +132,7 @@ class FrontController { $loader = new FilesystemLoader('../src/Views/'); $twig = new Environment($loader); $twig->display($file, $args); - } catch (RuntimeError | SyntaxError | LoaderError $e) { + } catch (RuntimeError|SyntaxError|LoaderError $e) { http_response_code(500); echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); throw $e; diff --git a/src/Controller/Sub/TeamController.php b/src/Controller/TeamController.php similarity index 68% rename from src/Controller/Sub/TeamController.php rename to src/Controller/TeamController.php index adefa41..6edb847 100644 --- a/src/Controller/Sub/TeamController.php +++ b/src/Controller/TeamController.php @@ -1,11 +1,12 @@ model = $model; } - public function displaySubmitTeam(): HttpResponse { + public function displayCreateTeam(SessionHandle $session): HttpResponse { return ViewHttpResponse::twig("insert_team.html.twig", []); } - public function displayAddMember() : HttpResponse { + public function displayAddMember(SessionHandle $session): HttpResponse { return ViewHttpResponse::twig("add_member.html.twig", []); } - public function displayDeleteMember() : HttpResponse { + public function displayDeleteMember(SessionHandle $session): HttpResponse { return ViewHttpResponse::twig("delete_member.html.twig", []); } - public function submitTeam(array $request): HttpResponse { + public function submitTeam(array $request, SessionHandle $session): HttpResponse { $errors = []; @@ -51,10 +52,10 @@ class TeamController { } return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]); } - return $this->getTeam($this->model->createTeam($request['name'], $request['picture'], $request['mainColor'], $request['secondColor'])); + return $this->displayTeam($this->model->createTeam($request['name'], $request['picture'], $request['mainColor'], $request['secondColor']), $session); } - public function displayListTeamByName(): HttpResponse { + public function displayListTeamByName(SessionHandle $session): HttpResponse { return ViewHttpResponse::twig("list_team_by_name.html.twig", []); } @@ -62,7 +63,7 @@ class TeamController { * @param array $request * @return HttpResponse */ - public function listTeamByName(array $request): HttpResponse { + public function listTeamByName(array $request, SessionHandle $session): HttpResponse { $errors = []; $request = HttpRequest::from($request, $errors, [ "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], @@ -82,30 +83,30 @@ class TeamController { return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $results]); } - public function getTeam(int $id): HttpResponse { + public function displayTeam(int $id, SessionHandle $session): HttpResponse { $result = $this->model->displayTeam($id); return ViewHttpResponse::twig('display_team.html.twig', ['team' => $result]); } - public function addMember(array $request) : HttpResponse { + public function addMember(array $request, SessionHandle $session): HttpResponse { $errors = []; $request = HttpRequest::from($request, $errors, [ "team" => [Validators::isInteger()], - "mail" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)] + "mail" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)] ]); - return $this->getTeam($this->model->addMember($request['mail'], intval($request['team']), $request['role'])); + return $this->displayTeam($this->model->addMember($request['mail'], intval($request['team']), $request['role']), $session); } - public function deleteMember(array $request) : HttpResponse { + public function deleteMember(array $request, SessionHandle $session): HttpResponse { $errors = []; $request = HttpRequest::from($request, $errors, [ "team" => [Validators::isInteger()], - "mail" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)] + "mail" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)] ]); - return $this->getTeam($this->model->deleteMember($request['mail'], intval($request['team']))); + return $this->displayTeam($this->model->deleteMember($request['mail'], intval($request['team'])), $session); } } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 51b6d97..3868aa4 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -3,86 +3,38 @@ namespace App\Controller; use App\Connexion; -use App\Controller\Sub\EditorController; use App\Gateway\TacticInfoGateway; -use App\Gateway\TeamGateway; use App\Http\HttpResponse; use App\Http\ViewHttpResponse; use App\Model\TacticModel; -use App\Model\TeamModel; use App\Session\SessionHandle; -class UserController extends VisitorController { - public function home(): HttpResponse { - $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); - $listTactic = $model->getLast(5); - return ViewHttpResponse::twig("home.twig", ["recentTactic" => $listTactic]); - } - - public function settings(): HttpResponse { - return ViewHttpResponse::twig("account_settings.twig", []); - } - - public function view(int $id, SessionHandle $session): HttpResponse { - $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); - return (new Sub\VisualizerController($model))->visualize($id, $session); - } - - public function edit(int $id, SessionHandle $session): HttpResponse { - $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); - return (new EditorController($model))->edit($id, $session); - } +class UserController { - public function create(SessionHandle $session): HttpResponse { - $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); - return (new EditorController($model))->createNew($session); - } - - public function open(int $id, SessionHandle $session): HttpResponse { - $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); - return (new Sub\VisualizerController($model))->visualize($id, $session); - } - - public function createTeam(): HttpResponse { - $model = new TeamModel(new TeamGateway(new Connexion(get_database()))); - $ctrl = new Sub\TeamController($model); - if ($_SERVER['REQUEST_METHOD'] === 'GET') { - return $ctrl->displaySubmitTeam(); - } - return $ctrl->submitTeam($_POST); - } + private TacticModel $tactics; - public function listTeams(): HttpResponse { - $model = new TeamModel(new TeamGateway(new Connexion(get_database()))); - $ctrl = new Sub\TeamController($model); - if ($_SERVER['REQUEST_METHOD'] === 'GET') { - return $ctrl->displayListTeamByName(); - } - return $ctrl->listTeamByName($_POST); + /** + * @param TacticModel $tactics + */ + public function __construct(TacticModel $tactics) { + $this->tactics = $tactics; } - public function getTeam(int $id): HttpResponse { - $model = new TeamModel(new TeamGateway(new Connexion(get_database()))); - $ctrl = new Sub\TeamController($model); - return $ctrl->getTeam($id); - } - - public function addMember(): HttpResponse { - $model = new TeamModel(new TeamGateway(new Connexion(get_database()))); - $ctrl = new Sub\TeamController($model); - if ($_SERVER['REQUEST_METHOD'] === 'GET') { - return $ctrl->displayAddMember($_POST); - } - return $ctrl->addMember($_POST); + /** + * @param SessionHandle $session + * @return HttpResponse the home page + */ + public function home(SessionHandle $session): HttpResponse { + //TODO use session's account to get the last 5 tactics if the logged-in account + $listTactic = $this->tactics->getLast(5); + return ViewHttpResponse::twig("home.twig", ["recentTactic" => $listTactic]); } - public function deleteMember(): HttpResponse { - $model = new TeamModel(new TeamGateway(new Connexion(get_database()))); - $ctrl = new Sub\TeamController($model); - if ($_SERVER['REQUEST_METHOD'] === 'GET') { - return $ctrl->displayDeleteMember($_POST); - } - return $ctrl->deleteMember($_POST); + /** + * @return HttpResponse account settings page + */ + public function settings(SessionHandle $session): HttpResponse { + return ViewHttpResponse::twig("account_settings.twig", []); } -} +} \ No newline at end of file diff --git a/src/Controller/VisitorController.php b/src/Controller/VisitorController.php deleted file mode 100644 index e74934a..0000000 --- a/src/Controller/VisitorController.php +++ /dev/null @@ -1,28 +0,0 @@ -displayRegister(); - } - return (new Sub\AuthController($model))->confirmRegister($_POST, $session); - } - - final public function login(MutableSessionHandle $session): HttpResponse { - $model = new AuthModel(new AccountGateway(new Connexion(get_database()))); - if ($_SERVER['REQUEST_METHOD'] === 'GET') { - return (new Sub\AuthController($model))->displayLogin(); - } - return (new Sub\AuthController($model))->confirmLogin($_POST, $session); - } - -} diff --git a/src/Controller/Sub/VisualizerController.php b/src/Controller/VisualizerController.php similarity index 90% rename from src/Controller/Sub/VisualizerController.php rename to src/Controller/VisualizerController.php index c7e5098..9b971c6 100644 --- a/src/Controller/Sub/VisualizerController.php +++ b/src/Controller/VisualizerController.php @@ -1,14 +1,12 @@ tacticModel = $tacticModel; } diff --git a/src/Data/Color.php b/src/Data/Color.php index b12e27a..b2da83b 100755 --- a/src/Data/Color.php +++ b/src/Data/Color.php @@ -16,9 +16,6 @@ class Color { */ private function __construct(string $value) { - if ($value < 0 || $value > 0xFFFFFF) { - throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF"); - } $this->hex = $value; } diff --git a/src/Views/add_member.html.twig b/src/Views/add_member.html.twig index ef74da2..6c5a3e3 100644 --- a/src/Views/add_member.html.twig +++ b/src/Views/add_member.html.twig @@ -73,7 +73,7 @@

Ajouter un membre à votre équipe

- +
diff --git a/src/Views/delete_member.html.twig b/src/Views/delete_member.html.twig index a478284..b7d0d3b 100644 --- a/src/Views/delete_member.html.twig +++ b/src/Views/delete_member.html.twig @@ -56,7 +56,7 @@

Supprimez un membre de votre équipe

- +
diff --git a/src/Views/display_teams.html.twig b/src/Views/display_teams.html.twig index bf89909..e4da64e 100644 --- a/src/Views/display_teams.html.twig +++ b/src/Views/display_teams.html.twig @@ -10,7 +10,7 @@

Aucune équipe n'a été trouvée

Chercher une équipe

- +
@@ -22,7 +22,7 @@
{% else %} {% for t in teams %} -
+

Nom de l'équipe : {{ t.name }}

logo de l'équipe
diff --git a/src/Views/home.twig b/src/Views/home.twig index 35379ce..29d228a 100644 --- a/src/Views/home.twig +++ b/src/Views/home.twig @@ -52,9 +52,9 @@

IQ Ball

-
+
Account logo

Mon profil

@@ -63,7 +63,7 @@

Mes équipes

- + {% if recentTeam != null %} {% for team in recentTeam %} @@ -77,13 +77,13 @@

Mes strategies

- + {% if recentTactic != null %} {% for tactic in recentTactic %} -
+

{{tactic.id}} - {{tactic.name}} - {{tactic.creation_date}}

- +
{% endfor %} {% else %} diff --git a/src/Views/insert_team.html.twig b/src/Views/insert_team.html.twig index 0fc59ca..0cc85dd 100644 --- a/src/Views/insert_team.html.twig +++ b/src/Views/insert_team.html.twig @@ -64,7 +64,7 @@

Créer une équipe

- +
diff --git a/src/Views/list_team_by_name.html.twig b/src/Views/list_team_by_name.html.twig index 0ea1905..09768e6 100644 --- a/src/Views/list_team_by_name.html.twig +++ b/src/Views/list_team_by_name.html.twig @@ -62,7 +62,7 @@

Chercher une équipe

- +
From de75577f3dc88c2600a089445b8969f40f17ce56 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Fri, 24 Nov 2023 10:20:45 +0100 Subject: [PATCH 61/75] divide src in three subdirs, exploded FrontController into index.php and api/index.php --- composer.json | 2 +- phpstan.neon | 5 +- public/api/index.php | 127 ++++--------- public/index.php | 179 ++++++++++++------ .../Api/APIAuthController.php | 16 +- .../Api/APITacticController.php | 20 +- src/Api/ApiAction.php | 46 +++++ src/App/AppAction.php | 55 ++++++ src/{ => App}/Controller/AuthController.php | 22 +-- src/{ => App}/Controller/EditorController.php | 18 +- src/{ => App}/Controller/TeamController.php | 45 +++-- src/{ => App}/Controller/UserController.php | 15 +- .../Controller/VisualizerController.php | 16 +- src/{ => App}/Views/account_settings.twig | 0 src/{ => App}/Views/add_member.html.twig | 0 src/{ => App}/Views/delete_member.html.twig | 0 .../Views/display_auth_confirm.html.twig | 0 src/{ => App}/Views/display_login.html.twig | 0 .../Views/display_register.html.twig | 0 src/{ => App}/Views/display_results.html.twig | 0 src/{ => App}/Views/display_team.html.twig | 0 src/{ => App}/Views/display_teams.html.twig | 0 src/{ => App}/Views/error.html.twig | 0 src/{ => App}/Views/home.twig | 2 +- src/{ => App}/Views/insert_team.html.twig | 0 .../Views/list_team_by_name.html.twig | 0 src/{ => App}/react-display-file.php | 0 src/{ => App}/react-display.php | 0 src/Controller/Route/Action.php | 71 ------- src/Controller/Route/FrontController.php | 143 -------------- src/{Connexion.php => Core/Connection.php} | 4 +- src/{ => Core}/Data/Account.php | 2 +- src/{ => Core}/Data/AccountUser.php | 2 +- src/{ => Core}/Data/Color.php | 2 +- src/{ => Core}/Data/Member.php | 7 +- src/{ => Core}/Data/MemberRole.php | 4 +- src/{ => Core}/Data/TacticInfo.php | 2 +- src/{ => Core}/Data/Team.php | 4 +- src/{ => Core}/Data/User.php | 2 +- src/{ => Core}/Gateway/AccountGateway.php | 12 +- src/{ => Core}/Gateway/AuthGateway.php | 10 +- src/{ => Core}/Gateway/TacticInfoGateway.php | 14 +- src/{ => Core}/Gateway/TeamGateway.php | 48 +++-- src/{ => Core}/Http/HttpCodes.php | 2 +- src/{ => Core}/Http/HttpRequest.php | 10 +- src/{ => Core}/Http/HttpResponse.php | 2 +- src/{ => Core}/Http/JsonHttpResponse.php | 2 +- src/{ => Core}/Http/ViewHttpResponse.php | 2 +- src/{ => Core}/Model/AuthModel.php | 10 +- src/{ => Core}/Model/TacticModel.php | 11 +- src/{ => Core}/Model/TeamModel.php | 34 ++-- src/Core/Route/AbstractAction.php | 36 ++++ src/{Controller => Core/Route}/Control.php | 18 +- .../Session/MutableSessionHandle.php | 4 +- src/{ => Core}/Session/PhpSessionHandle.php | 4 +- src/{ => Core}/Session/SessionHandle.php | 4 +- .../Validation/ComposedValidator.php | 2 +- .../Validation/FieldValidationFail.php | 2 +- .../Validation/FunctionValidator.php | 2 +- .../Validation/SimpleFunctionValidator.php | 2 +- src/{ => Core}/Validation/Validation.php | 2 +- src/{ => Core}/Validation/ValidationFail.php | 2 +- src/{ => Core}/Validation/Validator.php | 2 +- src/{ => Core}/Validation/Validators.php | 2 +- src/{ => Core}/Validator/TacticValidator.php | 6 +- {public => src}/utils.php | 0 66 files changed, 501 insertions(+), 555 deletions(-) rename src/{Controller => }/Api/APIAuthController.php (77%) rename src/{Controller => }/Api/APITacticController.php (85%) create mode 100644 src/Api/ApiAction.php create mode 100644 src/App/AppAction.php rename src/{ => App}/Controller/AuthController.php (86%) rename src/{ => App}/Controller/EditorController.php (80%) rename src/{ => App}/Controller/TeamController.php (79%) rename src/{ => App}/Controller/UserController.php (80%) rename src/{ => App}/Controller/VisualizerController.php (74%) rename src/{ => App}/Views/account_settings.twig (100%) rename src/{ => App}/Views/add_member.html.twig (100%) rename src/{ => App}/Views/delete_member.html.twig (100%) rename src/{ => App}/Views/display_auth_confirm.html.twig (100%) rename src/{ => App}/Views/display_login.html.twig (100%) rename src/{ => App}/Views/display_register.html.twig (100%) rename src/{ => App}/Views/display_results.html.twig (100%) rename src/{ => App}/Views/display_team.html.twig (100%) rename src/{ => App}/Views/display_teams.html.twig (100%) rename src/{ => App}/Views/error.html.twig (100%) rename src/{ => App}/Views/home.twig (97%) rename src/{ => App}/Views/insert_team.html.twig (100%) rename src/{ => App}/Views/list_team_by_name.html.twig (100%) rename src/{ => App}/react-display-file.php (100%) rename src/{ => App}/react-display.php (100%) delete mode 100644 src/Controller/Route/Action.php delete mode 100644 src/Controller/Route/FrontController.php rename src/{Connexion.php => Core/Connection.php} (97%) rename src/{ => Core}/Data/Account.php (97%) rename src/{ => Core}/Data/AccountUser.php (97%) rename src/{ => Core}/Data/Color.php (97%) rename src/{ => Core}/Data/Member.php (88%) rename src/{ => Core}/Data/MemberRole.php (94%) rename src/{ => Core}/Data/TacticInfo.php (97%) rename src/{ => Core}/Data/Team.php (95%) rename src/{ => Core}/Data/User.php (93%) rename src/{ => Core}/Gateway/AccountGateway.php (90%) rename src/{ => Core}/Gateway/AuthGateway.php (86%) rename src/{ => Core}/Gateway/TacticInfoGateway.php (87%) rename src/{ => Core}/Gateway/TeamGateway.php (68%) rename src/{ => Core}/Http/HttpCodes.php (90%) rename src/{ => Core}/Http/HttpRequest.php (92%) rename src/{ => Core}/Http/HttpResponse.php (97%) rename src/{ => Core}/Http/JsonHttpResponse.php (95%) rename src/{ => Core}/Http/ViewHttpResponse.php (98%) rename src/{ => Core}/Model/AuthModel.php (91%) rename src/{ => Core}/Model/TacticModel.php (89%) rename src/{ => Core}/Model/TeamModel.php (72%) create mode 100644 src/Core/Route/AbstractAction.php rename src/{Controller => Core/Route}/Control.php (90%) rename src/{ => Core}/Session/MutableSessionHandle.php (86%) rename src/{ => Core}/Session/PhpSessionHandle.php (92%) rename src/{ => Core}/Session/SessionHandle.php (89%) rename src/{ => Core}/Validation/ComposedValidator.php (94%) rename src/{ => Core}/Validation/FieldValidationFail.php (97%) rename src/{ => Core}/Validation/FunctionValidator.php (94%) rename src/{ => Core}/Validation/SimpleFunctionValidator.php (96%) rename src/{ => Core}/Validation/Validation.php (96%) rename src/{ => Core}/Validation/ValidationFail.php (96%) rename src/{ => Core}/Validation/Validator.php (95%) rename src/{ => Core}/Validation/Validators.php (98%) rename src/{ => Core}/Validator/TacticValidator.php (80%) rename {public => src}/utils.php (100%) diff --git a/composer.json b/composer.json index a3c0e4b..1d3a4d7 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "autoload": { "psr-4": { - "App\\": "src/" + "IQBall\\": "src/" } }, "require": { diff --git a/phpstan.neon b/phpstan.neon index 715fe84..346baaa 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,13 +3,10 @@ parameters: level: 6 paths: - src - - public scanFiles: - config.php - sql/database.php - profiles/dev-config-profile.php - profiles/prod-config-profile.php - - public/api/index.php excludePaths: - - src/react-display-file.php - - public/api/index.php + - src/App/react-display-file.php diff --git a/public/api/index.php b/public/api/index.php index cb278df..eeac0c1 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -5,84 +5,39 @@ require "../../vendor/autoload.php"; require "../../sql/database.php"; require "../utils.php"; -use App\Connexion; -use App\Controller\Api\APIAuthController; -use App\Controller\Api\APITacticController; -use App\Data\Account; -use App\Gateway\AccountGateway; -use App\Gateway\TacticInfoGateway; -use App\Http\HttpResponse; -use App\Http\JsonHttpResponse; -use App\Http\ViewHttpResponse; -use App\Model\AuthModel; -use App\Model\TacticModel; -use App\Session\PhpSessionHandle; -use App\Validation\ValidationFail; +use IQBall\Api\ApiAction; +use IQBall\Core\Connection; +use IQBall\Api\APIAuthController; +use IQBall\Api\APITacticController; +use IQBall\Core\Data\Account; +use IQBall\Core\Gateway\AccountGateway; +use IQBall\Core\Gateway\TacticInfoGateway; +use IQBall\Core\Http\HttpResponse; +use IQBall\Core\Http\JsonHttpResponse; +use IQBall\Core\Http\ViewHttpResponse; +use IQBall\Core\Model\AuthModel; +use IQBall\Core\Model\TacticModel; +use IQBall\Core\Session\PhpSessionHandle; +use IQBall\Core\Validation\ValidationFail; function getTacticController(): APITacticController { - return new APITacticController(new TacticModel(new TacticInfoGateway(new Connexion(get_database())))); + return new APITacticController(new TacticModel(new TacticInfoGateway(new Connection(get_database())))); } function getAuthController(): APIAuthController { - return new APIAuthController(new AuthModel(new AccountGateway(new Connexion(get_database())))); + return new APIAuthController(new AuthModel(new AccountGateway(new Connection(get_database())))); } -/** - * A Front controller action - */ -//TODO workaround for generic Action -class ApiAction { - /** - * @var callable(mixed[]): HttpResponse $action action to call - */ - private $action; - - private bool $isAuthRequired; - - /** - * @param callable(mixed[]): HttpResponse $action - */ - private function __construct(callable $action, bool $isAuthRequired) { - $this->action = $action; - $this->isAuthRequired = $isAuthRequired; - } +function getRoutes(): AltoRouter { + $router = new AltoRouter(); + $router->setBasePath(get_public_path() . "/api"); - public function isAuthRequired(): bool { - return $this->isAuthRequired; - } + $router->map("POST", "/tactic/[i:id]/edit/name", ApiAction::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); + $router->map("GET", "/tactic/[i:id]", ApiAction::auth(fn(int $id, Account $acc) => getTacticController()->getTacticInfo($id, $acc))); + $router->map("POST", "/tactic/new", ApiAction::auth(fn(Account $acc) => getTacticController()->newTactic($acc))); + $router->map("POST", "/auth", ApiAction::noAuth(fn() => getAuthController()->authorize())); - /** - * @param mixed[] $params - * @param ?Account $account - * @return HttpResponse - */ - public function run(array $params, ?Account $account): HttpResponse { - $params = array_values($params); - if ($this->isAuthRequired) { - if ($account == null) { - throw new Exception("action requires authorization."); - } - $params[] = $account; - } - - return call_user_func_array($this->action, $params); - } - - /** - * @param callable(mixed[]): HttpResponse $action - * @return ApiAction an action that does not require to have an authorization. - */ - public static function noAuth(callable $action): ApiAction { - return new ApiAction($action, false); - } - - /** - * @param callable(mixed[]): HttpResponse $action - * @return ApiAction an action that does require to have an authorization. - */ - public static function auth(callable $action): ApiAction { - return new ApiAction($action, true); - } + return $router; } /** @@ -97,7 +52,7 @@ function handleMatch(array $match): HttpResponse { $action = $match['target']; if (!$action instanceof ApiAction) { - throw new Exception("routed action is not an Action object."); + throw new Exception("routed action is not an AppAction object."); } $auth = null; @@ -122,27 +77,23 @@ function tryGetAuthorization(): ?Account { } $token = $headers['Authorization']; - $gateway = new AccountGateway(new Connexion(get_database())); + $gateway = new AccountGateway(new Connection(get_database())); return $gateway->getAccountFromToken($token); } -$router = new AltoRouter(); -$router->setBasePath(get_public_path() . "/api"); - -$router->map("POST", "/tactic/[i:id]/edit/name", ApiAction::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); -$router->map("GET", "/tactic/[i:id]", ApiAction::auth(fn(int $id, Account $acc) => getTacticController()->getTacticInfo($id, $acc))); -$router->map("POST", "/tactic/new", ApiAction::auth(fn(Account $acc) => getTacticController()->newTactic($acc))); -$router->map("POST", "/auth", ApiAction::noAuth(fn() => getAuthController()->authorize())); - -$match = $router->match(); +function render(HttpResponse $response): void { + http_response_code($response->getCode()); + foreach ($response->getHeaders() as $header => $value) { + header("$header: $value"); + } -$response = handleMatch($match); -http_response_code($response->getCode()); - -if ($response instanceof JsonHttpResponse) { - header('Content-type: application/json'); - echo $response->getJson(); -} elseif ($response instanceof ViewHttpResponse) { - throw new Exception("API returned a view http response."); + if ($response instanceof JsonHttpResponse) { + header('Content-type: application/json'); + echo $response->getJson(); + } elseif ($response instanceof ViewHttpResponse) { + throw new Exception("API returned a view http response."); + } } + +render(handleMatch(getRoutes()->match())); diff --git a/public/index.php b/public/index.php index 8d1f148..07d58c1 100644 --- a/public/index.php +++ b/public/index.php @@ -4,96 +4,157 @@ require "../vendor/autoload.php"; require "../config.php"; require "../sql/database.php"; -require "utils.php"; -require "../src/react-display.php"; - -use App\Controller\AuthController; -use App\Controller\EditorController; -use App\Controller\Route\Action; -use App\Controller\Route\FrontController; -use App\Controller\TeamController; -use App\Controller\UserController; -use App\Controller\VisualizerController; -use App\Gateway\AccountGateway; -use App\Gateway\TacticInfoGateway; -use App\Gateway\TeamGateway; -use App\Model\AuthModel; -use App\Model\TacticModel; -use App\Model\TeamModel; -use App\Session\MutableSessionHandle; -use App\Session\PhpSessionHandle; -use App\Connexion; -use App\Session\SessionHandle; - -$connexion = new Connexion(get_database()); +require "../src/utils.php"; +require "../src/App/react-display.php"; + +use IQBall\App\AppAction; +use IQBall\App\Controller\AuthController; +use IQBall\App\Controller\EditorController; +use IQBall\App\Controller\TeamController; +use IQBall\App\Controller\UserController; +use IQBall\App\Controller\VisualizerController; +use IQBall\Core\Connection; +use IQBall\Core\Gateway\AccountGateway; +use IQBall\Core\Gateway\TacticInfoGateway; +use IQBall\Core\Gateway\TeamGateway; +use IQBall\Core\Http\HttpCodes; +use IQBall\Core\Http\HttpResponse; +use IQBall\Core\Http\JsonHttpResponse; +use IQBall\Core\Http\ViewHttpResponse; +use IQBall\Core\Model\AuthModel; +use IQBall\Core\Model\TacticModel; +use IQBall\Core\Model\TeamModel; +use IQBall\Core\Session\MutableSessionHandle; +use IQBall\Core\Session\PhpSessionHandle; +use IQBall\Core\Session\SessionHandle; +use IQBall\Core\Validation\ValidationFail; +use Twig\Environment; +use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; +use Twig\Loader\FilesystemLoader; + +function getConnection(): Connection { + return new Connection(get_database()); +} function getUserController(): UserController { - global $connexion; - return new UserController(new TacticModel(new TacticInfoGateway($connexion))); + return new UserController(new TacticModel(new TacticInfoGateway(getConnection()))); } function getVisualizerController(): VisualizerController { - global $connexion; - return new VisualizerController(new TacticModel(new TacticInfoGateway($connexion))); + return new VisualizerController(new TacticModel(new TacticInfoGateway(getConnection()))); } function getEditorController(): EditorController { - global $connexion; - return new EditorController(new TacticModel(new TacticInfoGateway($connexion))); + return new EditorController(new TacticModel(new TacticInfoGateway(getConnection()))); } function getTeamController(): TeamController { - global $connexion; - return new TeamController(new TeamModel(new TeamGateway($connexion))); + return new TeamController(new TeamModel(new TeamGateway(getConnection()))); } function getAuthController(): AuthController { - global $connexion; - return new AuthController(new AuthModel(new AccountGateway($connexion))); + return new AuthController(new AuthModel(new AccountGateway(getConnection()))); } -function initFrontController(FrontController $fc) { +function getRoutes(): AltoRouter { + global $basePath; + + $ar = new AltoRouter(); + $ar->setBasePath($basePath); + //authentication - $fc->addRoute("GET", "/login", Action::noAuth(fn() => getAuthController()->displayLogin())); - $fc->addRoute("GET", "/register", Action::noAuth(fn() => getAuthController()->displayRegister())); - $fc->addRoute("POST", "/login", Action::noAuth(fn(SessionHandle $s) => getAuthController()->confirmLogin($_POST, $s))); - $fc->addRoute("POST", "/register", Action::noAuth(fn(SessionHandle $s) => getAuthController()->confirmRegister($_POST, $s))); + $ar->map("GET", "/login", AppAction::noAuth(fn() => getAuthController()->displayLogin())); + $ar->map("GET", "/register", AppAction::noAuth(fn() => getAuthController()->displayRegister())); + $ar->map("POST", "/login", AppAction::noAuth(fn(SessionHandle $s) => getAuthController()->confirmLogin($_POST, $s))); + $ar->map("POST", "/register", AppAction::noAuth(fn(SessionHandle $s) => getAuthController()->confirmRegister($_POST, $s))); //user-related - $fc->addRoute("GET", "/home", Action::auth(fn(SessionHandle $s) => getUserController()->home($s))); - $fc->addRoute("GET", "/settings", Action::auth(fn(SessionHandle $s) => getUserController()->settings($s))); + $ar->map("GET", "/home", AppAction::auth(fn(SessionHandle $s) => getUserController()->home($s))); + $ar->map("GET", "/settings", AppAction::auth(fn(SessionHandle $s) => getUserController()->settings($s))); //tactic-related - $fc->addRoute("GET", "/tactic/[i:id]/view", Action::auth(fn(int $id, SessionHandle $s) => getVisualizerController()->visualize($id, $s))); - $fc->addRoute("GET", "/tactic/[i:id]/edit", Action::auth(fn(int $id, SessionHandle $s) => getEditorController()->edit($id, $s))); - $fc->addRoute("GET", "/tactic/new", Action::auth(fn(SessionHandle $s) => getEditorController()->createNew($s))); + $ar->map("GET", "/tactic/[i:id]/view", AppAction::auth(fn(int $id, SessionHandle $s) => getVisualizerController()->visualize($id, $s))); + $ar->map("GET", "/tactic/[i:id]/edit", AppAction::auth(fn(int $id, SessionHandle $s) => getEditorController()->edit($id, $s))); + $ar->map("GET", "/tactic/new", AppAction::auth(fn(SessionHandle $s) => getEditorController()->createNew($s))); //team-related - $fc->addRoute("GET", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->displayCreateTeam($s))); - $fc->addRoute("POST", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->submitTeam($_POST, $s))); - $fc->addRoute("GET", "/team/search", Action::auth(fn(SessionHandle $s) => getTeamController()->displayListTeamByName($s))); - $fc->addRoute("POST", "/team/search", Action::auth(fn(SessionHandle $s) => getTeamController()->listTeamByName($_POST, $s))); - $fc->addRoute("GET", "/team/[i:id]", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->displayTeam($id, $s))); - $fc->addRoute("GET", "/team/members/add", Action::auth(fn(SessionHandle $s) => getTeamController()->displayAddMember($s))); - $fc->addRoute("POST", "/team/members/add", Action::auth(fn(SessionHandle $s) => getTeamController()->addMember($_POST, $s))); - $fc->addRoute("GET", "/team/members/remove", Action::auth(fn(SessionHandle $s) => getTeamController()->displayDeleteMember($s))); - $fc->addRoute("POST", "/team/members/remove", Action::auth(fn(SessionHandle $s) => getTeamController()->deleteMember($_POST, $s))); + $ar->map("GET", "/team/new", AppAction::auth(fn(SessionHandle $s) => getTeamController()->displayCreateTeam($s))); + $ar->map("POST", "/team/new", AppAction::auth(fn(SessionHandle $s) => getTeamController()->submitTeam($_POST, $s))); + $ar->map("GET", "/team/search", AppAction::auth(fn(SessionHandle $s) => getTeamController()->displayListTeamByName($s))); + $ar->map("POST", "/team/search", AppAction::auth(fn(SessionHandle $s) => getTeamController()->listTeamByName($_POST, $s))); + $ar->map("GET", "/team/[i:id]", AppAction::auth(fn(int $id, SessionHandle $s) => getTeamController()->displayTeam($id, $s))); + $ar->map("GET", "/team/members/add", AppAction::auth(fn(SessionHandle $s) => getTeamController()->displayAddMember($s))); + $ar->map("POST", "/team/members/add", AppAction::auth(fn(SessionHandle $s) => getTeamController()->addMember($_POST, $s))); + $ar->map("GET", "/team/members/remove", AppAction::auth(fn(SessionHandle $s) => getTeamController()->displayDeleteMember($s))); + $ar->map("POST", "/team/members/remove", AppAction::auth(fn(SessionHandle $s) => getTeamController()->deleteMember($_POST, $s))); + + return $ar; } -//this is a global variable -$basePath = get_public_path(); +function render(HttpResponse $response): void { + http_response_code($response->getCode()); + foreach ($response->getHeaders() as $header => $value) { + header("$header: $value"); + } -function run() { - global $basePath; + if ($response instanceof ViewHttpResponse) { + renderView($response); + } elseif ($response instanceof JsonHttpResponse) { + header('Content-type: application/json'); + echo $response->getJson(); + } +} - $fc = new FrontController($basePath); +function renderView(ViewHttpResponse $response): void { + $file = $response->getFile(); + $args = $response->getArguments(); + + switch ($response->getViewKind()) { + case ViewHttpResponse::REACT_VIEW: + send_react_front($file, $args); + break; + case ViewHttpResponse::TWIG_VIEW: + try { + $loader = new FilesystemLoader('../src/Views/'); + $twig = new Environment($loader); + $twig->display($file, $args); + } catch (RuntimeError | SyntaxError | LoaderError $e) { + http_response_code(500); + echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); + throw $e; + } + break; + } +} - initFrontController($fc); - $fc->run(PhpSessionHandle::init()); +function runAction(AppAction $action, array $params, MutableSessionHandle $session): HttpResponse { + global $basePath; + if ($action->isAuthRequired()) { + $account = $session->getAccount(); + if ($account == null) { + // put in the session the initial url the user wanted to get + $session->setInitialTarget($_SERVER['REQUEST_URI']); + return HttpResponse::redirect($basePath . "/login"); + } + } + + return $action->run($params, $session); } +function runMatch(array $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); + } -run(); + return runAction($match['target'], $match['params'], $session); +} +//this is a global variable +$basePath = get_public_path(); +render(runMatch(getRoutes()->match(), PhpSessionHandle::init())); diff --git a/src/Controller/Api/APIAuthController.php b/src/Api/APIAuthController.php similarity index 77% rename from src/Controller/Api/APIAuthController.php rename to src/Api/APIAuthController.php index d9e4c23..c4f5976 100644 --- a/src/Controller/Api/APIAuthController.php +++ b/src/Api/APIAuthController.php @@ -1,13 +1,13 @@ + */ +class ApiAction extends AbstractAction { + + /** + * @param mixed[] $params + * @param ?Account $session + * @return HttpResponse + */ + public function run(array $params, $session): HttpResponse { + $params = array_values($params); + if ($this->isAuthRequired()) { + if ($session == null) { + throw new \Exception("action requires authorization."); + } + $params[] = $session; + } + + return call_user_func_array($this->action, $params); + } + + /** + * @param callable(mixed[]): HttpResponse $action + * @return ApiAction an action that does not require to have an authorization. + */ + public static function noAuth(callable $action): ApiAction { + return new ApiAction($action, false); + } + + /** + * @param callable(mixed[]): HttpResponse $action + * @return ApiAction an action that does require to have an authorization. + */ + public static function auth(callable $action): ApiAction { + return new ApiAction($action, true); + } +} \ No newline at end of file diff --git a/src/App/AppAction.php b/src/App/AppAction.php new file mode 100644 index 0000000..0e4dcdf --- /dev/null +++ b/src/App/AppAction.php @@ -0,0 +1,55 @@ + + */ +class AppAction extends AbstractAction { + + /** + * @param mixed[] $params + * @param MutableSessionHandle $session + * @return HttpResponse + * @throws Exception

+ * thrown if this action is required to be authenticated, but the given session does not contain a logged-in account. + *

+ *

+ * Caller is supposed to ensure that the user is logged-in before, if `$this->isAuthRequired()` is true before + * running this action. + *

+ */ + public function run(array $params, $session): HttpResponse { + $params = array_values($params); + if ($this->isAuthRequired()) { + if ($session->getAccount() == null) { + throw new Exception("action requires authorization."); + } + } + + $params[] = $session; + return call_user_func_array($this->action, $params); + } + + /** + * @param callable(mixed[]): HttpResponse $action + * @return AppAction an action that does not require to have an authorization. + */ + public static function noAuth(callable $action): AppAction { + return new AppAction($action, false); + } + + /** + * @param callable(mixed[]): HttpResponse $action + * @return AppAction an action that does require to have an authorization. + */ + public static function auth(callable $action): AppAction { + return new AppAction($action, true); + } +} diff --git a/src/Controller/AuthController.php b/src/App/Controller/AuthController.php similarity index 86% rename from src/Controller/AuthController.php rename to src/App/Controller/AuthController.php index 0ee9a46..f74e45d 100644 --- a/src/Controller/AuthController.php +++ b/src/App/Controller/AuthController.php @@ -1,14 +1,14 @@ $fails]); } @@ -44,7 +44,7 @@ class AuthController { "username" => [Validators::name(), Validators::lenBetween(2, 32)], "password" => [Validators::lenBetween(6, 256)], "confirmpassword" => [Validators::lenBetween(6, 256)], - "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/","invalide"),Validators::lenBetween(5, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/", "invalide"),Validators::lenBetween(5, 256)], ]); if (!empty($fails)) { return $this->displayBadFields("display_register.html.twig", $fails); @@ -73,7 +73,7 @@ class AuthController { $fails = []; $request = HttpRequest::from($request, $fails, [ "password" => [Validators::lenBetween(6, 256)], - "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/","invalide"),Validators::lenBetween(5, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/", "invalide"),Validators::lenBetween(5, 256)], ]); if (!empty($fails)) { return $this->displayBadFields("display_login.html.twig", $fails); diff --git a/src/Controller/EditorController.php b/src/App/Controller/EditorController.php similarity index 80% rename from src/Controller/EditorController.php rename to src/App/Controller/EditorController.php index bfe80b0..0b47cba 100644 --- a/src/Controller/EditorController.php +++ b/src/App/Controller/EditorController.php @@ -1,14 +1,14 @@ $request + * @param SessionHandle $session + * @return HttpResponse + */ public function submitTeam(array $request, SessionHandle $session): HttpResponse { - - $errors = []; - - $request = HttpRequest::from($request, $errors, [ + $failures = []; + $request = HttpRequest::from($request, $failures, [ "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], "mainColor" => [Validators::regex('/#(?:[0-9a-fA-F]{6})/')], "secondColor" => [Validators::regex('/#(?:[0-9a-fA-F]{6})/')], "picture" => [Validators::isURL()], ]); - if (!empty($errors)) { + if (!empty($failures)) { $badFields = []; - foreach ($errors as $e) { + foreach ($failures as $e) { if ($e instanceof FieldValidationFail) { $badFields[] = $e->getFieldName(); } @@ -88,23 +91,33 @@ class TeamController { return ViewHttpResponse::twig('display_team.html.twig', ['team' => $result]); } + /** + * @param array $request + * @param SessionHandle $session + * @return HttpResponse + */ public function addMember(array $request, SessionHandle $session): HttpResponse { $errors = []; $request = HttpRequest::from($request, $errors, [ "team" => [Validators::isInteger()], - "mail" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)] + "mail" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)], ]); return $this->displayTeam($this->model->addMember($request['mail'], intval($request['team']), $request['role']), $session); } + /** + * @param array $request + * @param SessionHandle $session + * @return HttpResponse + */ public function deleteMember(array $request, SessionHandle $session): HttpResponse { $errors = []; $request = HttpRequest::from($request, $errors, [ "team" => [Validators::isInteger()], - "mail" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)] + "mail" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)], ]); return $this->displayTeam($this->model->deleteMember($request['mail'], intval($request['team'])), $session); diff --git a/src/Controller/UserController.php b/src/App/Controller/UserController.php similarity index 80% rename from src/Controller/UserController.php rename to src/App/Controller/UserController.php index 3868aa4..5d45038 100644 --- a/src/Controller/UserController.php +++ b/src/App/Controller/UserController.php @@ -1,16 +1,13 @@ IQ Ball

Account logo

Mon profil

diff --git a/src/Views/insert_team.html.twig b/src/App/Views/insert_team.html.twig similarity index 100% rename from src/Views/insert_team.html.twig rename to src/App/Views/insert_team.html.twig diff --git a/src/Views/list_team_by_name.html.twig b/src/App/Views/list_team_by_name.html.twig similarity index 100% rename from src/Views/list_team_by_name.html.twig rename to src/App/Views/list_team_by_name.html.twig diff --git a/src/react-display-file.php b/src/App/react-display-file.php similarity index 100% rename from src/react-display-file.php rename to src/App/react-display-file.php diff --git a/src/react-display.php b/src/App/react-display.php similarity index 100% rename from src/react-display.php rename to src/App/react-display.php diff --git a/src/Controller/Route/Action.php b/src/Controller/Route/Action.php deleted file mode 100644 index 61fd13a..0000000 --- a/src/Controller/Route/Action.php +++ /dev/null @@ -1,71 +0,0 @@ -action = $action; - $this->isAuthRequired = $isAuthRequired; - } - - public function isAuthRequired(): bool { - return $this->isAuthRequired; - } - - /** - * @param mixed[] $params - * @param SessionHandle $session - * @return HttpResponse - * @throws Exception

- * thrown if this action is required to be authenticated, but the given session does not contain a logged-in account. - *

- *

- * Caller is supposed to ensure that the user is logged-in before, if `$this->isAuthRequired()` is true before - * running this action. - *

- */ - public function run(array $params, SessionHandle $session): HttpResponse { - $params = array_values($params); - if ($this->isAuthRequired) { - if ($session->getAccount() == null) { - throw new Exception("action requires authorization."); - } - } - - $params[] = $session; - return call_user_func_array($this->action, $params); - } - - /** - * @param callable(mixed[]): HttpResponse $action - * @return Action an action that does not require to have an authorization. - */ - public static function noAuth(callable $action): Action { - return new Action($action, false); - } - - /** - * @param callable(mixed[]): HttpResponse $action - * @return Action an action that does require to have an authorization. - */ - public static function auth(callable $action): Action { - return new Action($action, true); - } -} \ No newline at end of file diff --git a/src/Controller/Route/FrontController.php b/src/Controller/Route/FrontController.php deleted file mode 100644 index 05b440e..0000000 --- a/src/Controller/Route/FrontController.php +++ /dev/null @@ -1,143 +0,0 @@ -router = $this->createRouter($basePath); - $this->basePath = $basePath; - } - - public function addRoute(string $method, string $path, Action $action): void { - $this->router->map($method, $path, $action); - } - - /** - * @param MutableSessionHandle $session - * @return void - * @throws LoaderError - * @throws RuntimeError - * @throws SyntaxError - */ - public function run(MutableSessionHandle $session): void { - $match = $this->router->match(); - if ($match) { - $this->handleMatch($match, $session); - return; - } - - $this->displayViewByKind(ViewHttpResponse::twig("error.html.twig", [ - 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")], - ], HttpCodes::NOT_FOUND)); - } - - /** - * Create a new instance of an AltoRouter - * - * @param string $basePath - * @return AltoRouter - */ - public function createRouter(string $basePath): AltoRouter { - $router = new AltoRouter(); - $router->setBasePath($basePath); - return $router; - } - - /** - * @param array $match - * @param MutableSessionHandle $session - * @return void - */ - private function handleMatch(array $match, MutableSessionHandle $session): void { - $action = $match['target']; - $params = array_values($match["params"]); - $this->handleResponseByType($this->tryToCall($action, $params, $session)); - } - - - /** - * @param Action $action - * @param array $params - * @param MutableSessionHandle $session - * @return HttpResponse - */ - private function tryToCall(Action $action, array $params, MutableSessionHandle $session): HttpResponse { - $account = null; - if ($action->isAuthRequired()) { - $account = $session->getAccount(); - if ($account == null) { - // put in the session the initial url the user wanted to get - $session->setInitialTarget($_SERVER['REQUEST_URI']); - return HttpResponse::redirect($this->basePath . "/login"); - } - } - - return $action->run($params, $session); - } - - /** - * Redirect the return by the response's type - * - * @param HttpResponse $response - * @return void - */ - private function handleResponseByType(HttpResponse $response): void { - http_response_code($response->getCode()); - - foreach ($response->getHeaders() as $header => $value) { - header("$header: $value"); - } - - if ($response instanceof ViewHttpResponse) { - $this->displayViewByKind($response); - } elseif ($response instanceof JsonHttpResponse) { - header('Content-type: application/json'); - echo $response->getJson(); - } - } - - /** - * Use the right method to display the response - * - * @param ViewHttpResponse $response - * @return void - */ - private function displayViewByKind(ViewHttpResponse $response): void { - $file = $response->getFile(); - $args = $response->getArguments(); - - switch ($response->getViewKind()) { - case ViewHttpResponse::REACT_VIEW: - send_react_front($file, $args); - break; - case ViewHttpResponse::TWIG_VIEW: - try { - $loader = new FilesystemLoader('../src/Views/'); - $twig = new Environment($loader); - $twig->display($file, $args); - } catch (RuntimeError|SyntaxError|LoaderError $e) { - http_response_code(500); - echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); - throw $e; - } - break; - } - } -} diff --git a/src/Connexion.php b/src/Core/Connection.php similarity index 97% rename from src/Connexion.php rename to src/Core/Connection.php index 987d35b..019b515 100644 --- a/src/Connexion.php +++ b/src/Core/Connection.php @@ -1,10 +1,10 @@ teamId; } } diff --git a/src/Data/MemberRole.php b/src/Core/Data/MemberRole.php similarity index 94% rename from src/Data/MemberRole.php rename to src/Core/Data/MemberRole.php index 559d516..7b63064 100755 --- a/src/Data/MemberRole.php +++ b/src/Core/Data/MemberRole.php @@ -1,8 +1,8 @@ members; } - public function addMember(Member $m) { + public function addMember(Member $m): void { $this->members[] = $m; } diff --git a/src/Data/User.php b/src/Core/Data/User.php similarity index 93% rename from src/Data/User.php rename to src/Core/Data/User.php index 6cb55c2..004379d 100755 --- a/src/Data/User.php +++ b/src/Core/Data/User.php @@ -1,6 +1,6 @@ con = $con; } diff --git a/src/Gateway/AuthGateway.php b/src/Core/Gateway/AuthGateway.php similarity index 86% rename from src/Gateway/AuthGateway.php rename to src/Core/Gateway/AuthGateway.php index 3370d26..a748f39 100644 --- a/src/Gateway/AuthGateway.php +++ b/src/Core/Gateway/AuthGateway.php @@ -1,17 +1,17 @@ con = $con; } diff --git a/src/Gateway/TacticInfoGateway.php b/src/Core/Gateway/TacticInfoGateway.php similarity index 87% rename from src/Gateway/TacticInfoGateway.php rename to src/Core/Gateway/TacticInfoGateway.php index 56e9b42..2031d04 100644 --- a/src/Gateway/TacticInfoGateway.php +++ b/src/Core/Gateway/TacticInfoGateway.php @@ -1,18 +1,18 @@ con = $con; } @@ -38,7 +38,7 @@ class TacticInfoGateway { * @param integer $nb * @return array> */ - public function getLast(int $nb) : ?array { + public function getLast(int $nb): ?array { $res = $this->con->fetch( "SELECT * FROM Tactic ORDER BY creation_date DESC LIMIT :nb ", [":nb" => [$nb, PDO::PARAM_INT]] diff --git a/src/Gateway/TeamGateway.php b/src/Core/Gateway/TeamGateway.php similarity index 68% rename from src/Gateway/TeamGateway.php rename to src/Core/Gateway/TeamGateway.php index c98d803..942d2a4 100644 --- a/src/Gateway/TeamGateway.php +++ b/src/Core/Gateway/TeamGateway.php @@ -1,14 +1,14 @@ con = $con; } @@ -25,18 +25,22 @@ class TeamGateway { } - public function insertMember(int $idTeam, int $idMember, string $role) { + 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] + ":role" => [$role, PDO::PARAM_STR], ] ); } + /** + * @param string $name + * @return array[] + */ public function listByName(string $name): array { return $this->con->fetch( "SELECT id,name,picture,mainColor,secondColor FROM Team WHERE name LIKE '%' || :name || '%'", @@ -48,28 +52,28 @@ class TeamGateway { /** * @param int $id - * @return array[] + * @return array */ - public function getTeamById(int $id): array { + public function getTeamById(int $id): ?array { return $this->con->fetch( "SELECT id,name,picture,mainColor,secondColor FROM Team WHERE id = :id", [ ":id" => [$id, PDO::PARAM_INT], ] - ); + )[0] ?? null; } /** * @param string $name - * @return array[] + * @return int|null */ - public function getIdTeamByName(string $name): array { + public function getIdTeamByName(string $name): ?int { return $this->con->fetch( "SELECT id FROM Team WHERE name = :name", [ - ":name" => [$name, PDO::PARAM_STR], + ":name" => [$name, PDO::PARAM_INT], ] - ); + )[0]['id'] ?? null; } /** @@ -78,23 +82,27 @@ class TeamGateway { */ 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] + "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], ] ); } - public function getMemberIdByMail($mail) : array { + /** + * @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] + ":mail" => [$mail, PDO::PARAM_STR], ] - ); + )[0]['id'] ?? null; } - public function deleteMember(int $idTeam, int $idMember) { + public function deleteMember(int $idTeam, int $idMember): void { $this->con->exec( "DELETE FROM Member WHERE idTeam = :idTeam AND idMember = :idMember", [ diff --git a/src/Http/HttpCodes.php b/src/Core/Http/HttpCodes.php similarity index 90% rename from src/Http/HttpCodes.php rename to src/Core/Http/HttpCodes.php index c2b01df..d0a00c5 100644 --- a/src/Http/HttpCodes.php +++ b/src/Core/Http/HttpCodes.php @@ -1,6 +1,6 @@ > */ - public function getLast(int $nb) : ?array { + public function getLast(int $nb): ?array { return $this->tactics->getLast($nb); } diff --git a/src/Model/TeamModel.php b/src/Core/Model/TeamModel.php similarity index 72% rename from src/Model/TeamModel.php rename to src/Core/Model/TeamModel.php index c936d30..fc2afc8 100644 --- a/src/Model/TeamModel.php +++ b/src/Core/Model/TeamModel.php @@ -1,12 +1,12 @@ gateway->insert($name, $picture, $mainColor, $secondColor); - $result = $this->gateway->getIdTeamByName($name); - return intval($result[0]['id']); + return $this->gateway->getIdTeamByName($name); } - - public function addMember(string $mail, int $teamId, string $role) : int { - $result = $this->gateway->getMemberIdByMail($mail)[0]; - $memberId = intval($result['id']); - $this->gateway->insertMember($teamId, $memberId, $role); + public function addMember(string $mail, int $teamId, string $role): int { + $id = $this->gateway->getMemberIdByMail($mail); + $this->gateway->insertMember($teamId, $id, $role); return $teamId; } + /** + * @param string $name + * @return Team[] + */ public function listByName(string $name): array { $teams = []; $results = $this->gateway->listByName($name); @@ -43,7 +44,7 @@ class TeamModel { public function displayTeam(int $id): Team { $members = []; - $result = $this->gateway->getTeamById($id)[0]; + $result = $this->gateway->getTeamById($id); $resultMembers = $this->gateway->getMembersById($id); foreach ($resultMembers as $row) { var_dump($row['role']); @@ -57,9 +58,8 @@ class TeamModel { return new Team(intval($result['id']), $result['name'], $result['picture'], Color::from($result['mainColor']), Color::from($result['secondColor']), $members); } - public function deleteMember(string $mail, int $teamId) : int { - $result = $this->gateway->getMemberIdByMail($mail)[0]; - $memberId = intval($result['id']); + public function deleteMember(string $mail, int $teamId): int { + $memberId = $this->gateway->getMemberIdByMail($mail); $this->gateway->deleteMember($teamId, $memberId); return $teamId; } diff --git a/src/Core/Route/AbstractAction.php b/src/Core/Route/AbstractAction.php new file mode 100644 index 0000000..443d071 --- /dev/null +++ b/src/Core/Route/AbstractAction.php @@ -0,0 +1,36 @@ +action = $action; + $this->isAuthRequired = $isAuthRequired; + } + + public function isAuthRequired(): bool { + return $this->isAuthRequired; + } + + /** + * @param mixed[] $params + * @param S $session + * @return HttpResponse + */ + public abstract function run(array $params, $session): HttpResponse; +} \ No newline at end of file diff --git a/src/Controller/Control.php b/src/Core/Route/Control.php similarity index 90% rename from src/Controller/Control.php rename to src/Core/Route/Control.php index 2b428d9..f29557d 100644 --- a/src/Controller/Control.php +++ b/src/Core/Route/Control.php @@ -1,14 +1,14 @@ Date: Fri, 24 Nov 2023 11:10:08 +0100 Subject: [PATCH 62/75] simplify actions, move ViewHttpResponse into src/App --- public/api/index.php | 76 ++---------- public/index.php | 113 ++++-------------- src/Api/API.php | 71 +++++++++++ src/Api/ApiAction.php | 46 ------- .../{ => Controller}/APIAuthController.php | 2 +- .../{ => Controller}/APITacticController.php | 23 +--- src/App/App.php | 90 ++++++++++++++ src/App/AppAction.php | 55 --------- src/{Core/Route => App}/Control.php | 2 +- src/App/Controller/AuthController.php | 2 +- src/App/Controller/EditorController.php | 2 +- src/App/Controller/TeamController.php | 2 +- src/App/Controller/UserController.php | 2 +- src/App/Controller/VisualizerController.php | 2 +- src/{Core/Http => App}/ViewHttpResponse.php | 5 +- src/Core/Action.php | 56 +++++++++ src/Core/Route/AbstractAction.php | 36 ------ 17 files changed, 258 insertions(+), 327 deletions(-) create mode 100644 src/Api/API.php delete mode 100644 src/Api/ApiAction.php rename src/Api/{ => Controller}/APIAuthController.php (96%) rename src/Api/{ => Controller}/APITacticController.php (60%) create mode 100644 src/App/App.php delete mode 100644 src/App/AppAction.php rename src/{Core/Route => App}/Control.php (98%) rename src/{Core/Http => App}/ViewHttpResponse.php (95%) create mode 100644 src/Core/Action.php delete mode 100644 src/Core/Route/AbstractAction.php diff --git a/public/api/index.php b/public/api/index.php index eeac0c1..076dd11 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -5,20 +5,16 @@ require "../../vendor/autoload.php"; require "../../sql/database.php"; require "../utils.php"; -use IQBall\Api\ApiAction; +use IQBall\Api\API; +use IQBall\Api\Controller\APIAuthController; +use IQBall\Api\Controller\APITacticController; +use IQBall\Core\Action; use IQBall\Core\Connection; -use IQBall\Api\APIAuthController; -use IQBall\Api\APITacticController; use IQBall\Core\Data\Account; use IQBall\Core\Gateway\AccountGateway; use IQBall\Core\Gateway\TacticInfoGateway; -use IQBall\Core\Http\HttpResponse; -use IQBall\Core\Http\JsonHttpResponse; -use IQBall\Core\Http\ViewHttpResponse; use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\TacticModel; -use IQBall\Core\Session\PhpSessionHandle; -use IQBall\Core\Validation\ValidationFail; function getTacticController(): APITacticController { return new APITacticController(new TacticModel(new TacticInfoGateway(new Connection(get_database())))); @@ -32,68 +28,10 @@ function getRoutes(): AltoRouter { $router = new AltoRouter(); $router->setBasePath(get_public_path() . "/api"); - $router->map("POST", "/tactic/[i:id]/edit/name", ApiAction::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); - $router->map("GET", "/tactic/[i:id]", ApiAction::auth(fn(int $id, Account $acc) => getTacticController()->getTacticInfo($id, $acc))); - $router->map("POST", "/tactic/new", ApiAction::auth(fn(Account $acc) => getTacticController()->newTactic($acc))); - $router->map("POST", "/auth", ApiAction::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", "/auth", Action::noAuth(fn() => getAuthController()->authorize())); return $router; } -/** - * @param mixed[] $match - * @return HttpResponse - * @throws Exception - */ -function handleMatch(array $match): HttpResponse { - if (!$match) { - return new JsonHttpResponse([ValidationFail::notFound("not found")]); - } - - $action = $match['target']; - if (!$action instanceof ApiAction) { - throw new Exception("routed action is not an AppAction object."); - } - - $auth = null; - - if ($action->isAuthRequired()) { - $auth = tryGetAuthorization(); - if ($auth == null) { - return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")]); - } - } - - return $action->run($match['params'], $auth); -} - -function tryGetAuthorization(): ?Account { - $headers = getallheaders(); - - // If no authorization header is set, try fallback to php session. - if (!isset($headers['Authorization'])) { - $session = PhpSessionHandle::init(); - return $session->getAccount(); - } - - $token = $headers['Authorization']; - $gateway = new AccountGateway(new Connection(get_database())); - return $gateway->getAccountFromToken($token); -} - -function render(HttpResponse $response): void { - http_response_code($response->getCode()); - - foreach ($response->getHeaders() as $header => $value) { - header("$header: $value"); - } - - if ($response instanceof JsonHttpResponse) { - header('Content-type: application/json'); - echo $response->getJson(); - } elseif ($response instanceof ViewHttpResponse) { - throw new Exception("API returned a view http response."); - } -} - -render(handleMatch(getRoutes()->match())); +Api::render(API::handleMatch(getRoutes()->match())); diff --git a/public/index.php b/public/index.php index 07d58c1..705284d 100644 --- a/public/index.php +++ b/public/index.php @@ -7,32 +7,22 @@ require "../sql/database.php"; require "../src/utils.php"; require "../src/App/react-display.php"; -use IQBall\App\AppAction; +use IQBall\App\App; use IQBall\App\Controller\AuthController; use IQBall\App\Controller\EditorController; use IQBall\App\Controller\TeamController; use IQBall\App\Controller\UserController; use IQBall\App\Controller\VisualizerController; +use IQBall\Core\Action; use IQBall\Core\Connection; use IQBall\Core\Gateway\AccountGateway; use IQBall\Core\Gateway\TacticInfoGateway; use IQBall\Core\Gateway\TeamGateway; -use IQBall\Core\Http\HttpCodes; -use IQBall\Core\Http\HttpResponse; -use IQBall\Core\Http\JsonHttpResponse; -use IQBall\Core\Http\ViewHttpResponse; use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TeamModel; -use IQBall\Core\Session\MutableSessionHandle; use IQBall\Core\Session\PhpSessionHandle; use IQBall\Core\Session\SessionHandle; -use IQBall\Core\Validation\ValidationFail; -use Twig\Environment; -use Twig\Error\LoaderError; -use Twig\Error\RuntimeError; -use Twig\Error\SyntaxError; -use Twig\Loader\FilesystemLoader; function getConnection(): Connection { return new Connection(get_database()); @@ -65,96 +55,37 @@ function getRoutes(): AltoRouter { $ar->setBasePath($basePath); //authentication - $ar->map("GET", "/login", AppAction::noAuth(fn() => getAuthController()->displayLogin())); - $ar->map("GET", "/register", AppAction::noAuth(fn() => getAuthController()->displayRegister())); - $ar->map("POST", "/login", AppAction::noAuth(fn(SessionHandle $s) => getAuthController()->confirmLogin($_POST, $s))); - $ar->map("POST", "/register", AppAction::noAuth(fn(SessionHandle $s) => getAuthController()->confirmRegister($_POST, $s))); + $ar->map("GET", "/login", Action::noAuth(fn() => getAuthController()->displayLogin())); + $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", "/register", Action::noAuth(fn(SessionHandle $s) => getAuthController()->confirmRegister($_POST, $s))); //user-related - $ar->map("GET", "/home", AppAction::auth(fn(SessionHandle $s) => getUserController()->home($s))); - $ar->map("GET", "/settings", AppAction::auth(fn(SessionHandle $s) => getUserController()->settings($s))); + $ar->map("GET", "/home", Action::auth(fn(SessionHandle $s) => getUserController()->home($s))); + $ar->map("GET", "/", Action::auth(fn(SessionHandle $s) => getUserController()->home($s))); + $ar->map("GET", "/settings", Action::auth(fn(SessionHandle $s) => getUserController()->settings($s))); //tactic-related - $ar->map("GET", "/tactic/[i:id]/view", AppAction::auth(fn(int $id, SessionHandle $s) => getVisualizerController()->visualize($id, $s))); - $ar->map("GET", "/tactic/[i:id]/edit", AppAction::auth(fn(int $id, SessionHandle $s) => getEditorController()->edit($id, $s))); - $ar->map("GET", "/tactic/new", AppAction::auth(fn(SessionHandle $s) => getEditorController()->createNew($s))); + $ar->map("GET", "/tactic/[i:id]/view", Action::auth(fn(int $id, SessionHandle $s) => getVisualizerController()->visualize($id, $s))); + $ar->map("GET", "/tactic/[i:id]/edit", Action::auth(fn(int $id, SessionHandle $s) => getEditorController()->edit($id, $s))); + $ar->map("GET", "/tactic/new", Action::auth(fn(SessionHandle $s) => getEditorController()->createNew($s))); //team-related - $ar->map("GET", "/team/new", AppAction::auth(fn(SessionHandle $s) => getTeamController()->displayCreateTeam($s))); - $ar->map("POST", "/team/new", AppAction::auth(fn(SessionHandle $s) => getTeamController()->submitTeam($_POST, $s))); - $ar->map("GET", "/team/search", AppAction::auth(fn(SessionHandle $s) => getTeamController()->displayListTeamByName($s))); - $ar->map("POST", "/team/search", AppAction::auth(fn(SessionHandle $s) => getTeamController()->listTeamByName($_POST, $s))); - $ar->map("GET", "/team/[i:id]", AppAction::auth(fn(int $id, SessionHandle $s) => getTeamController()->displayTeam($id, $s))); - $ar->map("GET", "/team/members/add", AppAction::auth(fn(SessionHandle $s) => getTeamController()->displayAddMember($s))); - $ar->map("POST", "/team/members/add", AppAction::auth(fn(SessionHandle $s) => getTeamController()->addMember($_POST, $s))); - $ar->map("GET", "/team/members/remove", AppAction::auth(fn(SessionHandle $s) => getTeamController()->displayDeleteMember($s))); - $ar->map("POST", "/team/members/remove", AppAction::auth(fn(SessionHandle $s) => getTeamController()->deleteMember($_POST, $s))); + $ar->map("GET", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->displayCreateTeam($s))); + $ar->map("POST", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->submitTeam($_POST, $s))); + $ar->map("GET", "/team/search", Action::auth(fn(SessionHandle $s) => getTeamController()->displayListTeamByName($s))); + $ar->map("POST", "/team/search", Action::auth(fn(SessionHandle $s) => getTeamController()->listTeamByName($_POST, $s))); + $ar->map("GET", "/team/[i:id]", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->displayTeam($id, $s))); + $ar->map("GET", "/team/members/add", Action::auth(fn(SessionHandle $s) => getTeamController()->displayAddMember($s))); + $ar->map("POST", "/team/members/add", Action::auth(fn(SessionHandle $s) => getTeamController()->addMember($_POST, $s))); + $ar->map("GET", "/team/members/remove", Action::auth(fn(SessionHandle $s) => getTeamController()->displayDeleteMember($s))); + $ar->map("POST", "/team/members/remove", Action::auth(fn(SessionHandle $s) => getTeamController()->deleteMember($_POST, $s))); return $ar; } -function render(HttpResponse $response): void { - http_response_code($response->getCode()); - - foreach ($response->getHeaders() as $header => $value) { - header("$header: $value"); - } - - if ($response instanceof ViewHttpResponse) { - renderView($response); - } elseif ($response instanceof JsonHttpResponse) { - header('Content-type: application/json'); - echo $response->getJson(); - } -} - -function renderView(ViewHttpResponse $response): void { - $file = $response->getFile(); - $args = $response->getArguments(); - - switch ($response->getViewKind()) { - case ViewHttpResponse::REACT_VIEW: - send_react_front($file, $args); - break; - case ViewHttpResponse::TWIG_VIEW: - try { - $loader = new FilesystemLoader('../src/Views/'); - $twig = new Environment($loader); - $twig->display($file, $args); - } catch (RuntimeError | SyntaxError | LoaderError $e) { - http_response_code(500); - echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); - throw $e; - } - break; - } -} - -function runAction(AppAction $action, array $params, MutableSessionHandle $session): HttpResponse { - global $basePath; - if ($action->isAuthRequired()) { - $account = $session->getAccount(); - if ($account == null) { - // put in the session the initial url the user wanted to get - $session->setInitialTarget($_SERVER['REQUEST_URI']); - return HttpResponse::redirect($basePath . "/login"); - } - } - - return $action->run($params, $session); -} - -function runMatch(array $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 runAction($match['target'], $match['params'], $session); -} //this is a global variable $basePath = get_public_path(); -render(runMatch(getRoutes()->match(), PhpSessionHandle::init())); +App::render(App::runMatch(getRoutes()->match(), PhpSessionHandle::init())); diff --git a/src/Api/API.php b/src/Api/API.php new file mode 100644 index 0000000..d266e79 --- /dev/null +++ b/src/Api/API.php @@ -0,0 +1,71 @@ +getCode()); + + foreach ($response->getHeaders() as $header => $value) { + header("$header: $value"); + } + + if ($response instanceof JsonHttpResponse) { + header('Content-type: application/json'); + echo $response->getJson(); + } else { + throw new Exception("API returned a non-json response."); + } + } + + /** + * @param mixed[] $match + * @return HttpResponse + * @throws Exception + */ + public static function handleMatch(array $match): HttpResponse { + if (!$match) { + return new JsonHttpResponse([ValidationFail::notFound("not found")]); + } + + $action = $match['target']; + if (!$action instanceof Action) { + throw new Exception("routed action is not an AppAction object."); + } + + $auth = null; + + if ($action->isAuthRequired()) { + $auth = self::tryGetAuthorization(); + if ($auth == null) { + return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")]); + } + } + + return $action->run($match['params'], $auth); + } + + private static function tryGetAuthorization(): ?Account { + $headers = getallheaders(); + + // If no authorization header is set, try fallback to php session. + if (!isset($headers['Authorization'])) { + $session = PhpSessionHandle::init(); + return $session->getAccount(); + } + + $token = $headers['Authorization']; + $gateway = new AccountGateway(new Connection(get_database())); + return $gateway->getAccountFromToken($token); + } +} diff --git a/src/Api/ApiAction.php b/src/Api/ApiAction.php deleted file mode 100644 index 9cfd0aa..0000000 --- a/src/Api/ApiAction.php +++ /dev/null @@ -1,46 +0,0 @@ - - */ -class ApiAction extends AbstractAction { - - /** - * @param mixed[] $params - * @param ?Account $session - * @return HttpResponse - */ - public function run(array $params, $session): HttpResponse { - $params = array_values($params); - if ($this->isAuthRequired()) { - if ($session == null) { - throw new \Exception("action requires authorization."); - } - $params[] = $session; - } - - return call_user_func_array($this->action, $params); - } - - /** - * @param callable(mixed[]): HttpResponse $action - * @return ApiAction an action that does not require to have an authorization. - */ - public static function noAuth(callable $action): ApiAction { - return new ApiAction($action, false); - } - - /** - * @param callable(mixed[]): HttpResponse $action - * @return ApiAction an action that does require to have an authorization. - */ - public static function auth(callable $action): ApiAction { - return new ApiAction($action, true); - } -} \ No newline at end of file diff --git a/src/Api/APIAuthController.php b/src/Api/Controller/APIAuthController.php similarity index 96% rename from src/Api/APIAuthController.php rename to src/Api/Controller/APIAuthController.php index c4f5976..59ec1d0 100644 --- a/src/Api/APIAuthController.php +++ b/src/Api/Controller/APIAuthController.php @@ -1,6 +1,6 @@ [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], - ], function (HttpRequest $request) use ($account) { - $tactic = $this->model->makeNew($request["name"], $account->getId()); - $id = $tactic->getId(); - return new JsonHttpResponse(["id" => $id]); - }, true); - } - - public function getTacticInfo(int $id, Account $account): HttpResponse { - $tactic_info = $this->model->get($id); - - if ($tactic_info == null) { - return new JsonHttpResponse("could not find tactic #$id", HttpCodes::NOT_FOUND); - } - - return new JsonHttpResponse($tactic_info); - } - } diff --git a/src/App/App.php b/src/App/App.php new file mode 100644 index 0000000..9126afd --- /dev/null +++ b/src/App/App.php @@ -0,0 +1,90 @@ +getCode()); + + foreach ($response->getHeaders() as $header => $value) { + header("$header: $value"); + } + + if ($response instanceof ViewHttpResponse) { + self::renderView($response); + } elseif ($response instanceof JsonHttpResponse) { + header('Content-type: application/json'); + echo $response->getJson(); + } + } + + private static function renderView(ViewHttpResponse $response): void { + $file = $response->getFile(); + $args = $response->getArguments(); + + switch ($response->getViewKind()) { + case ViewHttpResponse::REACT_VIEW: + send_react_front($file, $args); + break; + case ViewHttpResponse::TWIG_VIEW: + try { + $loader = new FilesystemLoader('../src/App/Views/'); + $twig = new Environment($loader); + $twig->display($file, $args); + } catch (RuntimeError | SyntaxError | LoaderError $e) { + http_response_code(500); + echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); + throw $e; + } + break; + } + } + + /** + * @param Action $action + * @param mixed[] $params + * @param MutableSessionHandle $session + * @return HttpResponse + */ + private static function runAction(Action $action, array $params, MutableSessionHandle $session): HttpResponse { + global $basePath; + if ($action->isAuthRequired()) { + $account = $session->getAccount(); + if ($account == null) { + // put in the session the initial url the user wanted to get + $session->setInitialTarget($_SERVER['REQUEST_URI']); + return HttpResponse::redirect($basePath . "/login"); + } + } + + return $action->run($params, $session); + } + + /** + * @param array|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); + } + +} diff --git a/src/App/AppAction.php b/src/App/AppAction.php deleted file mode 100644 index 0e4dcdf..0000000 --- a/src/App/AppAction.php +++ /dev/null @@ -1,55 +0,0 @@ - - */ -class AppAction extends AbstractAction { - - /** - * @param mixed[] $params - * @param MutableSessionHandle $session - * @return HttpResponse - * @throws Exception

- * thrown if this action is required to be authenticated, but the given session does not contain a logged-in account. - *

- *

- * Caller is supposed to ensure that the user is logged-in before, if `$this->isAuthRequired()` is true before - * running this action. - *

- */ - public function run(array $params, $session): HttpResponse { - $params = array_values($params); - if ($this->isAuthRequired()) { - if ($session->getAccount() == null) { - throw new Exception("action requires authorization."); - } - } - - $params[] = $session; - return call_user_func_array($this->action, $params); - } - - /** - * @param callable(mixed[]): HttpResponse $action - * @return AppAction an action that does not require to have an authorization. - */ - public static function noAuth(callable $action): AppAction { - return new AppAction($action, false); - } - - /** - * @param callable(mixed[]): HttpResponse $action - * @return AppAction an action that does require to have an authorization. - */ - public static function auth(callable $action): AppAction { - return new AppAction($action, true); - } -} diff --git a/src/Core/Route/Control.php b/src/App/Control.php similarity index 98% rename from src/Core/Route/Control.php rename to src/App/Control.php index f29557d..3dea9b1 100644 --- a/src/Core/Route/Control.php +++ b/src/App/Control.php @@ -2,11 +2,11 @@ namespace IQBall\Core\Route; +use IQBall\App\ViewHttpResponse; use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; -use IQBall\Core\Http\ViewHttpResponse; use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\Validator; diff --git a/src/App/Controller/AuthController.php b/src/App/Controller/AuthController.php index f74e45d..46ef838 100644 --- a/src/App/Controller/AuthController.php +++ b/src/App/Controller/AuthController.php @@ -4,7 +4,7 @@ namespace IQBall\App\Controller; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; -use IQBall\Core\Http\ViewHttpResponse; +use IQBall\App\ViewHttpResponse; use IQBall\Core\Model\AuthModel; use IQBall\Core\Session\MutableSessionHandle; use IQBall\Core\Validation\ValidationFail; diff --git a/src/App/Controller/EditorController.php b/src/App/Controller/EditorController.php index 0b47cba..b004e10 100644 --- a/src/App/Controller/EditorController.php +++ b/src/App/Controller/EditorController.php @@ -5,7 +5,7 @@ namespace IQBall\App\Controller; use IQBall\Core\Data\TacticInfo; use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpResponse; -use IQBall\Core\Http\ViewHttpResponse; +use IQBall\App\ViewHttpResponse; use IQBall\Core\Model\TacticModel; use IQBall\Core\Session\SessionHandle; use IQBall\Core\Validator\TacticValidator; diff --git a/src/App/Controller/TeamController.php b/src/App/Controller/TeamController.php index d8db9e9..c34bfa8 100644 --- a/src/App/Controller/TeamController.php +++ b/src/App/Controller/TeamController.php @@ -4,7 +4,7 @@ namespace IQBall\App\Controller; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; -use IQBall\Core\Http\ViewHttpResponse; +use IQBall\App\ViewHttpResponse; use IQBall\Core\Model\TeamModel; use IQBall\Core\Session\SessionHandle; use IQBall\Core\Validation\FieldValidationFail; diff --git a/src/App/Controller/UserController.php b/src/App/Controller/UserController.php index 5d45038..7fd1a8d 100644 --- a/src/App/Controller/UserController.php +++ b/src/App/Controller/UserController.php @@ -3,7 +3,7 @@ namespace IQBall\App\Controller; use IQBall\Core\Http\HttpResponse; -use IQBall\Core\Http\ViewHttpResponse; +use IQBall\App\ViewHttpResponse; use IQBall\Core\Model\TacticModel; use IQBall\Core\Session\SessionHandle; diff --git a/src/App/Controller/VisualizerController.php b/src/App/Controller/VisualizerController.php index 46946dc..22a4367 100644 --- a/src/App/Controller/VisualizerController.php +++ b/src/App/Controller/VisualizerController.php @@ -4,7 +4,7 @@ namespace IQBall\App\Controller; use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpResponse; -use IQBall\Core\Http\ViewHttpResponse; +use IQBall\App\ViewHttpResponse; use IQBall\Core\Model\TacticModel; use IQBall\Core\Session\SessionHandle; use IQBall\Core\Validator\TacticValidator; diff --git a/src/Core/Http/ViewHttpResponse.php b/src/App/ViewHttpResponse.php similarity index 95% rename from src/Core/Http/ViewHttpResponse.php rename to src/App/ViewHttpResponse.php index 1f7c692..dfbd1da 100644 --- a/src/Core/Http/ViewHttpResponse.php +++ b/src/App/ViewHttpResponse.php @@ -1,6 +1,9 @@ action = $action; + $this->isAuthRequired = $isAuthRequired; + } + + public function isAuthRequired(): bool { + return $this->isAuthRequired; + } + + /** + * @param mixed[] $params + * @param S $session + * @return HttpResponse + */ + public function run(array $params, $session): HttpResponse { + $params = array_values($params); + $params[] = $session; + return call_user_func_array($this->action, $params); + } + + /** + * @param callable(mixed[], S): HttpResponse $action + * @return Action an action that does not require to have an authorization. + */ + public static function noAuth(callable $action): Action { + return new Action($action, false); + } + + /** + * @param callable(mixed[], S): HttpResponse $action + * @return Action an action that does require to have an authorization. + */ + public static function auth(callable $action): Action { + return new Action($action, true); + } +} diff --git a/src/Core/Route/AbstractAction.php b/src/Core/Route/AbstractAction.php deleted file mode 100644 index 443d071..0000000 --- a/src/Core/Route/AbstractAction.php +++ /dev/null @@ -1,36 +0,0 @@ -action = $action; - $this->isAuthRequired = $isAuthRequired; - } - - public function isAuthRequired(): bool { - return $this->isAuthRequired; - } - - /** - * @param mixed[] $params - * @param S $session - * @return HttpResponse - */ - public abstract function run(array $params, $session): HttpResponse; -} \ No newline at end of file From 5b85ddce7da0c2b78227102ce7b0d21aea4c6729 Mon Sep 17 00:00:00 2001 From: samuel Date: Sat, 25 Nov 2023 19:00:22 +0100 Subject: [PATCH 63/75] model --- Documentation/models.puml | 33 ++++++++++++++++++++------------- Documentation/team.puml | 5 +++-- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/Documentation/models.puml b/Documentation/models.puml index 82037bc..3482ab1 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -1,14 +1,15 @@ @startuml class Account { - - email: String - - phoneNumber: String + - email: string + - phoneNumber: string - id: int - + setMailAddress(String) - + getMailAddress(): String - + getPhoneNumber(): String - + setPhoneNumber(String) + + __construct (email : string, ???) + + setMailAddress(string) + + getMailAddress(): string + + getPhoneNumber(): string + + setPhoneNumber(string) + getUser(): AccountUser + getId(): int } @@ -17,17 +18,18 @@ Account --> "- user" AccountUser Account --> "- teams *" Team interface User { - + getName(): String + + getName(): string + getProfilePicture(): Url + getAge(): int } class AccountUser { - - name: String + - name: string - profilePicture: Url - age: int - + setName(String) + + __construct(name : string, age : int, profilePicture : string) + + setName(string) + setProfilePicture(URI) + setAge(int) } @@ -36,6 +38,7 @@ AccountUser ..|> User class Member { - userId: int + + __construct(role : MemberRole) + getUserId(): int + getRole(): MemberRole } @@ -48,10 +51,11 @@ enum MemberRole { } class Team { - - name: String + - name: string - picture: Url - + getName(): String + + __construct(name : string, picture : string, mainColor : Color, secondColor : Color,members : array) + + getName(): string + getPicture(): Url + getMainColor(): Color + getSecondColor(): Color @@ -59,7 +63,7 @@ class Team { } Team --> "- mainColor" Color -Team --> "- secondaryColor" Color +Team --> "- secondColor" Color Team --> "- members *" Member class Color { @@ -70,6 +74,7 @@ class Color { class AuthController{ + + __construct(model : AuthModel) + displayRegister() : HttpResponse + displayBadFields(viewName : string, fails : array) : HttpResponse + confirmRegister(request : array) : HttpResponse @@ -80,6 +85,7 @@ 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) @@ -89,7 +95,8 @@ AuthModel --> "- gateway" AuthGateway class AuthGateway{ -con : Connection - + mailExists(email : string) : bool + + __construct (con : Connection) + + mailExist(email : string) : bool + insertAccount(username : string, hash : string, email : string) + getHash(email : string):string + getAccount (email : string): array diff --git a/Documentation/team.puml b/Documentation/team.puml index a291b1d..78b6659 100644 --- a/Documentation/team.puml +++ b/Documentation/team.puml @@ -1,10 +1,11 @@ @startuml class Team { - - name: String + - name: string - picture: Url - members: array - + getName(): String + + __construct(name : string, picture : string, mainColor : Colo, secondColor : Color) + + getName(): string + getPicture(): Url + getMainColor(): Color + getSecondColor(): Color From 89c3e72c8f4e3bb95ad4570da69c82a6640e8382 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sat, 25 Nov 2023 20:35:00 +0100 Subject: [PATCH 64/75] add and edit documentation, clean code --- Documentation/how-to-dev.md | 17 +--- Documentation/http.puml | 4 + Documentation/models.puml | 93 +++++++-------------- Documentation/mvc/auth.puml | 28 +++++++ Documentation/{ => mvc}/team.puml | 8 +- Documentation/php.puml | 8 -- Documentation/validation.puml | 2 + public/index.php | 30 +++++-- sql/setup-tables.sql | 14 ++-- src/Api/Controller/APIAuthController.php | 9 +- src/Api/Controller/APITacticController.php | 6 ++ src/App/App.php | 53 ++++++------ src/App/Controller/AuthController.php | 28 +++---- src/App/Controller/EditorController.php | 21 +++-- src/App/Controller/TeamController.php | 64 ++++++++++---- src/App/Controller/UserController.php | 10 +-- src/App/Controller/VisualizerController.php | 8 +- src/App/Views/display_team.html.twig | 24 +++--- src/App/Views/insert_team.html.twig | 8 +- src/Core/Action.php | 2 + src/Core/Data/AccountUser.php | 52 ------------ src/Core/Data/MemberRole.php | 23 ++++- src/Core/Data/TacticInfo.php | 19 ++--- src/Core/Data/Team.php | 59 ++----------- src/Core/Data/TeamInfo.php | 49 +++++++++++ src/Core/Data/User.php | 26 ------ src/Core/Gateway/AccountGateway.php | 13 ++- src/Core/Gateway/AuthGateway.php | 47 ----------- src/Core/Gateway/MemberGateway.php | 69 +++++++++++++++ src/Core/Gateway/TacticInfoGateway.php | 16 ++++ src/Core/Gateway/TeamGateway.php | 90 +++++++------------- src/Core/Http/HttpCodes.php | 1 + src/Core/Http/HttpResponse.php | 9 ++ src/Core/Model/AuthModel.php | 10 ++- src/Core/Model/TacticModel.php | 13 ++- src/Core/Model/TeamModel.php | 87 +++++++++++-------- src/Core/Validation/ValidationFail.php | 8 ++ src/Core/Validation/Validators.php | 17 +++- src/Core/Validator/TacticValidator.php | 7 +- 39 files changed, 563 insertions(+), 489 deletions(-) create mode 100644 Documentation/mvc/auth.puml rename Documentation/{ => mvc}/team.puml (90%) delete mode 100644 Documentation/php.puml delete mode 100755 src/Core/Data/AccountUser.php create mode 100644 src/Core/Data/TeamInfo.php delete mode 100755 src/Core/Data/User.php delete mode 100644 src/Core/Gateway/AuthGateway.php create mode 100644 src/Core/Gateway/MemberGateway.php diff --git a/Documentation/how-to-dev.md b/Documentation/how-to-dev.md index 0af8da5..39435e6 100644 --- a/Documentation/how-to-dev.md +++ b/Documentation/how-to-dev.md @@ -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. 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) 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 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()`). 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 ### 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 : ```php @@ -137,13 +137,4 @@ function _asset(string $assetURI): string { // fallback to the uri itself. 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 : +``` \ No newline at end of file diff --git a/Documentation/http.puml b/Documentation/http.puml index 9fbe606..2f8e22b 100644 --- a/Documentation/http.puml +++ b/Documentation/http.puml @@ -44,4 +44,8 @@ class ViewHttpResponse extends HttpResponse { + react(file: string, arguments: array, code: int = HttpCodes::OK): ViewHttpResponse } +note right of ViewHttpResponse + Into src/App +end note + @enduml \ No newline at end of file diff --git a/Documentation/models.puml b/Documentation/models.puml index 3482ab1..c0629d4 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -1,45 +1,36 @@ @startuml -class Account { - - email: string - - phoneNumber: string +class TacticInfo { - id: int - - + __construct (email : string, ???) - + setMailAddress(string) - + getMailAddress(): string - + getPhoneNumber(): string - + setPhoneNumber(string) - + getUser(): AccountUser + - name: string + - creationDate: string + - ownerId: string + + getId(): int -} - -Account --> "- user" AccountUser -Account --> "- teams *" Team - -interface User { + + getOwnerId(): int + + getCreationTimestamp(): int + getName(): string - + getProfilePicture(): Url - + getAge(): int } -class AccountUser { +class Account { + - email: string + - token: string - name: string - - profilePicture: Url - - age: int + - id: int - + __construct(name : string, age : int, profilePicture : string) - + setName(string) - + setProfilePicture(URI) - + setAge(int) + + getMailAddress(): string + + getToken(): string + + getName(): string + + getId(): int } -AccountUser ..|> User class Member { - userId: int + - teamId: int + __construct(role : MemberRole) + getUserId(): int + + getTeamId(): int + getRole(): MemberRole } @@ -50,20 +41,27 @@ enum MemberRole { COACH } -class Team { + +class TeamInfo { + - creationDate: int - name: string - - picture: Url + - picture: string - + __construct(name : string, picture : string, mainColor : Color, secondColor : Color,members : array) + getName(): string - + getPicture(): Url + + getPicture(): string + getMainColor(): Color + getSecondColor(): Color - + listMembers(): array } -Team --> "- mainColor" Color -Team --> "- secondColor" Color +TeamInfo --> "- mainColor" Color +TeamInfo --> "- secondaryColor" Color + +class Team { + getInfo(): TeamInfo + listMembers(): Member[] +} + +Team --> "- info" TeamInfo Team --> "- members *" Member class Color { @@ -72,33 +70,4 @@ class Color { + 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 \ No newline at end of file diff --git a/Documentation/mvc/auth.puml b/Documentation/mvc/auth.puml new file mode 100644 index 0000000..d416bc3 --- /dev/null +++ b/Documentation/mvc/auth.puml @@ -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 \ No newline at end of file diff --git a/Documentation/team.puml b/Documentation/mvc/team.puml similarity index 90% rename from Documentation/team.puml rename to Documentation/mvc/team.puml index 78b6659..ad5e201 100644 --- a/Documentation/team.puml +++ b/Documentation/mvc/team.puml @@ -58,12 +58,6 @@ class TeamController{ TeamController *--"- model" TeamModel -class Connexion{ - - pdo : PDO - -- - + __constructor(pdo : PDO) - + exec(query : string, args : array) - + fetch(query string, args array): array -} +class Connexion { } @enduml \ No newline at end of file diff --git a/Documentation/php.puml b/Documentation/php.puml deleted file mode 100644 index 8641900..0000000 --- a/Documentation/php.puml +++ /dev/null @@ -1,8 +0,0 @@ -@startuml - -class FrontController { - - router : AltoRouter - -} - -@enduml \ No newline at end of file diff --git a/Documentation/validation.puml b/Documentation/validation.puml index dd0cafe..f509cf4 100644 --- a/Documentation/validation.puml +++ b/Documentation/validation.puml @@ -50,9 +50,11 @@ class Validation { } class Validators { + --- + nonEmpty(): Validator + shorterThan(limit: int): Validator + userString(maxLen: int): Validator + ... } diff --git a/public/index.php b/public/index.php index 705284d..807ec88 100644 --- a/public/index.php +++ b/public/index.php @@ -13,16 +13,22 @@ use IQBall\App\Controller\EditorController; use IQBall\App\Controller\TeamController; use IQBall\App\Controller\UserController; use IQBall\App\Controller\VisualizerController; +use IQBall\App\ViewHttpResponse; use IQBall\Core\Action; use IQBall\Core\Connection; use IQBall\Core\Gateway\AccountGateway; +use IQBall\Core\Gateway\MemberGateway; use IQBall\Core\Gateway\TacticInfoGateway; use IQBall\Core\Gateway\TeamGateway; +use IQBall\Core\Http\HttpCodes; +use IQBall\Core\Http\HttpResponse; use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TeamModel; +use IQBall\Core\Session\MutableSessionHandle; use IQBall\Core\Session\PhpSessionHandle; use IQBall\Core\Session\SessionHandle; +use IQBall\Core\Validation\ValidationFail; function getConnection(): Connection { return new Connection(get_database()); @@ -41,7 +47,8 @@ function getEditorController(): EditorController { } 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 { @@ -57,8 +64,8 @@ function getRoutes(): AltoRouter { //authentication $ar->map("GET", "/login", Action::noAuth(fn() => getAuthController()->displayLogin())); $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", "/register", Action::noAuth(fn(SessionHandle $s) => getAuthController()->confirmRegister($_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()->register($_POST, $s))); //user-related $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))); //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]/edit", Action::auth(fn(int $id, SessionHandle $s) => getEditorController()->edit($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()->openEditor($id, $s))); $ar->map("GET", "/tactic/new", Action::auth(fn(SessionHandle $s) => getEditorController()->createNew($s))); //team-related @@ -84,8 +91,19 @@ function getRoutes(): AltoRouter { 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 $basePath = get_public_path(); -App::render(App::runMatch(getRoutes()->match(), PhpSessionHandle::init())); +App::render(runMatch(getRoutes()->match(), PhpSessionHandle::init()), "../src/App/Views/"); diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index b13712f..324fb39 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -30,15 +30,15 @@ CREATE TABLE Team id integer PRIMARY KEY AUTOINCREMENT, name varchar, picture varchar, - mainColor varchar, - secondColor varchar + main_color varchar, + second_color varchar ); CREATE TABLE Member( - idTeam integer, - idMember integer, - role char(1) CHECK (role IN ('C', 'P')), - FOREIGN KEY (idTeam) REFERENCES Team (id), - FOREIGN KEY (idMember) REFERENCES User (id) + id_team integer, + id_user integer, + role char(1) CHECK (role IN ('Coach', 'Player')), + FOREIGN KEY (id_team) REFERENCES Team (id), + FOREIGN KEY (id_user) REFERENCES User (id) ); diff --git a/src/Api/Controller/APIAuthController.php b/src/Api/Controller/APIAuthController.php index 59ec1d0..332f260 100644 --- a/src/Api/Controller/APIAuthController.php +++ b/src/Api/Controller/APIAuthController.php @@ -2,6 +2,7 @@ namespace IQBall\Api\Controller; +use IQBall\Core\Http\HttpCodes; use IQBall\Core\Route\Control; use IQBall\Core\Http\HttpRequest; 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 { 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)], ], function (HttpRequest $req) { $failures = []; $account = $this->model->login($req["email"], $req["password"], $failures); if (!empty($failures)) { - return new JsonHttpResponse($failures); + return new JsonHttpResponse($failures, HttpCodes::UNAUTHORIZED); } return new JsonHttpResponse(["authorization" => $account->getToken()]); diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index cba1fda..63e4811 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -24,6 +24,12 @@ class APITacticController { $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 { return Control::runChecked([ "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], diff --git a/src/App/App.php b/src/App/App.php index 9126afd..7fa2595 100644 --- a/src/App/App.php +++ b/src/App/App.php @@ -3,11 +3,9 @@ namespace IQBall\App; use IQBall\Core\Action; -use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Session\MutableSessionHandle; -use IQBall\Core\Validation\ValidationFail; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; @@ -15,7 +13,16 @@ use Twig\Error\SyntaxError; use Twig\Loader\FilesystemLoader; 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()); foreach ($response->getHeaders() as $header => $value) { @@ -23,14 +30,23 @@ class App { } if ($response instanceof ViewHttpResponse) { - self::renderView($response); + self::renderView($response, $twigViewsFolder); } elseif ($response instanceof JsonHttpResponse) { header('Content-type: application/json'); 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(); $args = $response->getArguments(); @@ -40,8 +56,8 @@ class App { break; case ViewHttpResponse::TWIG_VIEW: try { - $loader = new FilesystemLoader('../src/App/Views/'); - $twig = new Environment($loader); + $fl = new FilesystemLoader($twigViewsFolder); + $twig = new Environment($fl); $twig->display($file, $args); } catch (RuntimeError | SyntaxError | LoaderError $e) { 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 $action * @param mixed[] $params * @param MutableSessionHandle $session * @return HttpResponse */ - private static function runAction(Action $action, array $params, MutableSessionHandle $session): HttpResponse { - global $basePath; + public static function runAction(string $authRoute, Action $action, array $params, MutableSessionHandle $session): HttpResponse { if ($action->isAuthRequired()) { $account = $session->getAccount(); if ($account == null) { // put in the session the initial url the user wanted to get $session->setInitialTarget($_SERVER['REQUEST_URI']); - return HttpResponse::redirect($basePath . "/login"); + return HttpResponse::redirect($authRoute); } } return $action->run($params, $session); } - /** - * @param array|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); - } - } diff --git a/src/App/Controller/AuthController.php b/src/App/Controller/AuthController.php index 46ef838..9e790e9 100644 --- a/src/App/Controller/AuthController.php +++ b/src/App/Controller/AuthController.php @@ -25,33 +25,25 @@ class AuthController { } /** - * @param string $viewName - * @param ValidationFail[] $fails - * @return HttpResponse - */ - private function displayBadFields(string $viewName, array $fails): HttpResponse { - return ViewHttpResponse::twig($viewName, ['fails' => $fails]); - } - - /** + * registers given account * @param mixed[] $request * @param MutableSessionHandle $session * @return HttpResponse */ - public function confirmRegister(array $request, MutableSessionHandle $session): HttpResponse { + public function register(array $request, MutableSessionHandle $session): HttpResponse { $fails = []; $request = HttpRequest::from($request, $fails, [ "username" => [Validators::name(), Validators::lenBetween(2, 32)], "password" => [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)) { - 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); if (!empty($fails)) { - return $this->displayBadFields("display_register.html.twig", $fails); + return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]); } $session->setAccount($account); @@ -66,22 +58,24 @@ class AuthController { } /** + * logins given account credentials * @param mixed[] $request + * @param MutableSessionHandle $session * @return HttpResponse */ - public function confirmLogin(array $request, MutableSessionHandle $session): HttpResponse { + public function login(array $request, MutableSessionHandle $session): HttpResponse { $fails = []; $request = HttpRequest::from($request, $fails, [ "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)) { - 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); if (!empty($fails)) { - return $this->displayBadFields("display_login.html.twig", $fails); + return ViewHttpResponse::twig("display_login.html.twig", ['fails' => $fails]); } $session->setAccount($account); diff --git a/src/App/Controller/EditorController.php b/src/App/Controller/EditorController.php index b004e10..6724ced 100644 --- a/src/App/Controller/EditorController.php +++ b/src/App/Controller/EditorController.php @@ -17,22 +17,31 @@ class EditorController { $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()]); } - 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()); - return $this->openEditor($tactic); + return $this->openEditorFor($tactic); } /** * returns an editor view for a given tactic * @param int $id the targeted tactic identifier * @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); $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 $this->openEditor($tactic); + return $this->openEditorFor($tactic); } diff --git a/src/App/Controller/TeamController.php b/src/App/Controller/TeamController.php index c34bfa8..deaf61b 100644 --- a/src/App/Controller/TeamController.php +++ b/src/App/Controller/TeamController.php @@ -20,20 +20,34 @@ class TeamController { $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", []); } - 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", []); } - 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", []); } /** + * create a new team from given request name, mainColor, secondColor and picture url * @param array $request * @param SessionHandle $session * @return HttpResponse @@ -42,8 +56,8 @@ class TeamController { $failures = []; $request = HttpRequest::from($request, $failures, [ "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], - "mainColor" => [Validators::regex('/#(?:[0-9a-fA-F]{6})/')], - "secondColor" => [Validators::regex('/#(?:[0-9a-fA-F]{6})/')], + "main_color" => [Validators::hexColor()], + "second_color" => [Validators::hexColor()], "picture" => [Validators::isURL()], ]); if (!empty($failures)) { @@ -55,15 +69,22 @@ class TeamController { } 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", []); } /** - * @param array $request + * returns a view that contains all the teams description whose name matches the given name needle. + * @param array $request + * @param SessionHandle $session * @return 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]); } - $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', ['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]); } /** + * add a member to a team * @param array $request * @param SessionHandle $session * @return HttpResponse @@ -101,13 +128,16 @@ class TeamController { $request = HttpRequest::from($request, $errors, [ "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 $request * @param SessionHandle $session * @return HttpResponse @@ -117,9 +147,9 @@ class TeamController { $request = HttpRequest::from($request, $errors, [ "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); } } diff --git a/src/App/Controller/UserController.php b/src/App/Controller/UserController.php index 7fd1a8d..b7cef0b 100644 --- a/src/App/Controller/UserController.php +++ b/src/App/Controller/UserController.php @@ -19,18 +19,18 @@ class UserController { /** * @param SessionHandle $session - * @return HttpResponse the home page + * @return ViewHttpResponse the home page view */ - public function home(SessionHandle $session): HttpResponse { - //TODO use session's account to get the last 5 tactics if the logged-in account + public function home(SessionHandle $session): ViewHttpResponse { + //TODO use session's account to get the last 5 tactics of the logged-in account $listTactic = $this->tactics->getLast(5); 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", []); } diff --git a/src/App/Controller/VisualizerController.php b/src/App/Controller/VisualizerController.php index 22a4367..48e1168 100644 --- a/src/App/Controller/VisualizerController.php +++ b/src/App/Controller/VisualizerController.php @@ -19,7 +19,13 @@ class VisualizerController { $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); $failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId()); diff --git a/src/App/Views/display_team.html.twig b/src/App/Views/display_team.html.twig index 474397e..6f0aec1 100644 --- a/src/App/Views/display_team.html.twig +++ b/src/App/Views/display_team.html.twig @@ -20,17 +20,13 @@ height:50px; } - #mainColor{ - background-color: {{ team.mainColor.getValue() }}; - {% if team.mainColor.getValue() == "#ffffff" %} - border-color: #666666; - {% endif %} + #main_color { + border: solid; + background-color: {{ team.getInfo().getMainColor().getValue() }}; } - #secondColor{ - background-color: {{ team.secondColor.getValue() }}; - {% if team.secondColor.getValue() == "#ffffff" %} - border-color: #666666; - {% endif %} + #second_color{ + background-color: {{ team.getInfo().getSecondColor().getValue() }}; + border: solid; } .container{ @@ -66,12 +62,12 @@
-

{{ team.name }}

- +

{{ team.getInfo().getName() }}

+
-

Couleur principale :

-

Couleur secondaire :

+

Couleur principale :

+

Couleur secondaire :

{% for m in team.listMembers() %} diff --git a/src/App/Views/insert_team.html.twig b/src/App/Views/insert_team.html.twig index 0cc85dd..0eafb55 100644 --- a/src/App/Views/insert_team.html.twig +++ b/src/App/Views/insert_team.html.twig @@ -70,10 +70,10 @@ - - - - + + + +
diff --git a/src/Core/Action.php b/src/Core/Action.php index c93958a..35721c1 100644 --- a/src/Core/Action.php +++ b/src/Core/Action.php @@ -5,6 +5,7 @@ namespace IQBall\Core; use IQBall\Core\Http\HttpResponse; /** + * Represent an action. * @template S session */ class Action { @@ -28,6 +29,7 @@ class Action { } /** + * Runs an action * @param mixed[] $params * @param S $session * @return HttpResponse diff --git a/src/Core/Data/AccountUser.php b/src/Core/Data/AccountUser.php deleted file mode 100755 index 2f8f0da..0000000 --- a/src/Core/Data/AccountUser.php +++ /dev/null @@ -1,52 +0,0 @@ -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; - } - - -} diff --git a/src/Core/Data/MemberRole.php b/src/Core/Data/MemberRole.php index 7b63064..41b6b71 100755 --- a/src/Core/Data/MemberRole.php +++ b/src/Core/Data/MemberRole.php @@ -7,7 +7,7 @@ use InvalidArgumentException; /** * Enumeration class workaround * 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 { private const ROLE_PLAYER = 0; @@ -32,6 +32,27 @@ final class MemberRole { 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 { return ($val <= self::MAX and $val >= self::MIN); } diff --git a/src/Core/Data/TacticInfo.php b/src/Core/Data/TacticInfo.php index 6f31680..04f592a 100644 --- a/src/Core/Data/TacticInfo.php +++ b/src/Core/Data/TacticInfo.php @@ -2,24 +2,23 @@ namespace IQBall\Core\Data; -class TacticInfo implements \JsonSerializable { +class TacticInfo { private int $id; private string $name; - private int $creation_date; - + private int $creationDate; private int $ownerId; /** * @param int $id * @param string $name - * @param int $creation_date + * @param int $creationDate * @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->name = $name; $this->ownerId = $ownerId; - $this->creation_date = $creation_date; + $this->creationDate = $creationDate; } public function getId(): int { @@ -38,13 +37,7 @@ class TacticInfo implements \JsonSerializable { } public function getCreationTimestamp(): int { - return $this->creation_date; + return $this->creationDate; } - /** - * @return array - */ - public function jsonSerialize(): array { - return get_object_vars($this); - } } diff --git a/src/Core/Data/Team.php b/src/Core/Data/Team.php index 6366680..b8e7834 100755 --- a/src/Core/Data/Team.php +++ b/src/Core/Data/Team.php @@ -3,11 +3,7 @@ namespace IQBall\Core\Data; class Team { - private int $id; - private string $name; - private string $picture; - private Color $mainColor; - private Color $secondColor; + private TeamInfo $info; /** * @var Member[] maps users with their role @@ -15,54 +11,16 @@ class Team { private array $members; /** - * @param string $name - * @param string $picture - * @param Color $mainColor - * @param Color $secondColor + * @param TeamInfo $info * @param Member[] $members */ - public function __construct(int $id, string $name, string $picture, Color $mainColor, Color $secondColor, array $members = []) { - $this->id = $id; - $this->name = $name; - $this->picture = $picture; - $this->mainColor = $mainColor; - $this->secondColor = $secondColor; + public function __construct(TeamInfo $info, array $members = []) { + $this->info = $info; $this->members = $members; } - /** - * @return int - */ - 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; + public function getInfo(): TeamInfo { + return $this->info; } /** @@ -71,9 +29,4 @@ class Team { public function listMembers(): array { return $this->members; } - - public function addMember(Member $m): void { - $this->members[] = $m; - } - } diff --git a/src/Core/Data/TeamInfo.php b/src/Core/Data/TeamInfo.php new file mode 100644 index 0000000..7affcea --- /dev/null +++ b/src/Core/Data/TeamInfo.php @@ -0,0 +1,49 @@ +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; + } + + +} diff --git a/src/Core/Data/User.php b/src/Core/Data/User.php deleted file mode 100755 index 004379d..0000000 --- a/src/Core/Data/User.php +++ /dev/null @@ -1,26 +0,0 @@ -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 { $results = $this->getRowsFromMail($email); if ($results == null) { @@ -44,6 +47,10 @@ class AccountGateway { return $results['hash']; } + /** + * @param string $email + * @return bool true if the given email exists in the database + */ public function exists(string $email): bool { return $this->getRowsFromMail($email) != null; } @@ -61,6 +68,10 @@ class AccountGateway { 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 { $acc = $this->con->fetch("SELECT * FROM Account WHERE token = :token", [':token' => [$token, PDO::PARAM_STR]])[0] ?? null; if (empty($acc)) { diff --git a/src/Core/Gateway/AuthGateway.php b/src/Core/Gateway/AuthGateway.php deleted file mode 100644 index a748f39..0000000 --- a/src/Core/Gateway/AuthGateway.php +++ /dev/null @@ -1,47 +0,0 @@ -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|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; - } - - - - -} diff --git a/src/Core/Gateway/MemberGateway.php b/src/Core/Gateway/MemberGateway.php new file mode 100644 index 0000000..999bf10 --- /dev/null +++ b/src/Core/Gateway/MemberGateway.php @@ -0,0 +1,69 @@ +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], + ] + ); + } + +} diff --git a/src/Core/Gateway/TacticInfoGateway.php b/src/Core/Gateway/TacticInfoGateway.php index 2031d04..f3d2432 100644 --- a/src/Core/Gateway/TacticInfoGateway.php +++ b/src/Core/Gateway/TacticInfoGateway.php @@ -16,6 +16,11 @@ class TacticInfoGateway { $this->con = $con; } + /** + * get tactic information from given identifier + * @param int $id + * @return TacticInfo|null + */ public function get(int $id): ?TacticInfo { $res = $this->con->fetch( "SELECT * FROM Tactic WHERE id = :id", @@ -49,6 +54,11 @@ class TacticInfoGateway { return $res; } + /** + * @param string $name + * @param int $owner + * @return TacticInfo + */ public function insert(string $name, int $owner): TacticInfo { $this->con->exec( "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"]); } + /** + * update name of given tactic identifier + * @param int $id + * @param string $name + * @return void + */ public function updateName(int $id, string $name): void { $this->con->exec( "UPDATE Tactic SET name = :name WHERE id = :id", diff --git a/src/Core/Gateway/TeamGateway.php b/src/Core/Gateway/TeamGateway.php index 942d2a4..d775eda 100644 --- a/src/Core/Gateway/TeamGateway.php +++ b/src/Core/Gateway/TeamGateway.php @@ -3,6 +3,8 @@ namespace IQBall\Core\Gateway; use IQBall\Core\Connection; +use IQBall\Core\Data\Color; +use IQBall\Core\Data\TeamInfo; use PDO; class TeamGateway { @@ -12,62 +14,65 @@ class TeamGateway { $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( - "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], - ":mainColor" => [$mainColor, PDO::PARAM_STR], - ":secondColor" => [$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], + ":main_color" => [$mainColor, PDO::PARAM_STR], + ":second_color" => [$secondColor, PDO::PARAM_STR], ] ); + return intval($this->con->lastInsertId()); } /** * @param string $name - * @return array[] + * @return TeamInfo[] */ public function listByName(string $name): array { - return $this->con->fetch( - "SELECT id,name,picture,mainColor,secondColor FROM Team WHERE name LIKE '%' || :name || '%'", + $result = $this->con->fetch( + "SELECT * FROM Team WHERE name LIKE '%' || :name || '%'", [ ":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 - * @return array + * @return TeamInfo */ - public function getTeamById(int $id): ?array { - return $this->con->fetch( - "SELECT id,name,picture,mainColor,secondColor FROM Team WHERE id = :id", + public function getTeamById(int $id): ?TeamInfo { + $row = $this->con->fetch( + "SELECT * FROM Team WHERE id = :id", [ ":id" => [$id, PDO::PARAM_INT], ] )[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 * @return int|null */ - public function getIdTeamByName(string $name): ?int { + public function getTeamIdByName(string $name): ?int { return $this->con->fetch( "SELECT id FROM Team WHERE name = :name", [ @@ -76,40 +81,5 @@ class TeamGateway { )[0]['id'] ?? null; } - /** - * @param int $id - * @return array[] - */ - 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], - ] - ); - } } diff --git a/src/Core/Http/HttpCodes.php b/src/Core/Http/HttpCodes.php index d0a00c5..1903f0c 100644 --- a/src/Core/Http/HttpCodes.php +++ b/src/Core/Http/HttpCodes.php @@ -9,6 +9,7 @@ class HttpCodes { public const OK = 200; public const FOUND = 302; public const BAD_REQUEST = 400; + public const UNAUTHORIZED = 401; public const FORBIDDEN = 403; diff --git a/src/Core/Http/HttpResponse.php b/src/Core/Http/HttpResponse.php index 345b2ab..eb52ccc 100644 --- a/src/Core/Http/HttpResponse.php +++ b/src/Core/Http/HttpResponse.php @@ -29,10 +29,19 @@ class HttpResponse { return $this->headers; } + /** + * @param int $code + * @return HttpResponse + */ public static function fromCode(int $code): HttpResponse { 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 { if ($code < 300 || $code >= 400) { throw new \InvalidArgumentException("given code is not a redirection http code"); diff --git a/src/Core/Model/AuthModel.php b/src/Core/Model/AuthModel.php index 9044785..a6ada53 100644 --- a/src/Core/Model/AuthModel.php +++ b/src/Core/Model/AuthModel.php @@ -47,6 +47,10 @@ class AuthModel { return new Account($email, $username, $token, $accountId); } + /** + * Generate a random base 64 string + * @return string + */ private function generateToken(): string { return base64_encode(random_bytes(64)); } @@ -58,11 +62,11 @@ class AuthModel { * @return Account|null the authenticated account or null if failures occurred */ public function login(string $email, string $password, array &$failures): ?Account { - if (!$this->gateway->exists($email)) { - $failures[] = new FieldValidationFail("email", "Vous n'êtes pas enregistré."); + $hash = $this->gateway->getHash($email); + if ($hash == null) { + $failures[] = new FieldValidationFail("email", "l'addresse email n'est pas connue."); return null; } - $hash = $this->gateway->getHash($email); if (!password_verify($password, $hash)) { $failures[] = new FieldValidationFail("password", "Mot de passe invalide."); diff --git a/src/Core/Model/TacticModel.php b/src/Core/Model/TacticModel.php index a74685a..64c6ca3 100644 --- a/src/Core/Model/TacticModel.php +++ b/src/Core/Model/TacticModel.php @@ -19,12 +19,23 @@ class TacticModel { $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 { 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 { - return $this->tactics->insert(self::TACTIC_DEFAULT_NAME, $ownerId); + return $this->makeNew(self::TACTIC_DEFAULT_NAME, $ownerId); } /** diff --git a/src/Core/Model/TeamModel.php b/src/Core/Model/TeamModel.php index fc2afc8..f6af837 100644 --- a/src/Core/Model/TeamModel.php +++ b/src/Core/Model/TeamModel.php @@ -2,65 +2,80 @@ 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\Team; +use IQBall\Core\Data\TeamInfo; +use IQBall\Core\Gateway\AccountGateway; +use IQBall\Core\Gateway\MemberGateway; +use IQBall\Core\Gateway\TeamGateway; class TeamModel { - private TeamGateway $gateway; + private AccountGateway $users; + private TeamGateway $teams; + private MemberGateway $members; /** * @param TeamGateway $gateway + * @param MemberGateway $members + * @param AccountGateway $users */ - public function __construct(TeamGateway $gateway) { - $this->gateway = $gateway; + public function __construct(TeamGateway $gateway, MemberGateway $members, AccountGateway $users) { + $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 { - $this->gateway->insert($name, $picture, $mainColor, $secondColor); - return $this->gateway->getIdTeamByName($name); + return $this->teams->insert($name, $picture, $mainColor, $secondColor); } - public function addMember(string $mail, int $teamId, string $role): int { - $id = $this->gateway->getMemberIdByMail($mail); - $this->gateway->insertMember($teamId, $id, $role); - return $teamId; + /** + * adds a member to a team + * @param string $mail + * @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 - * @return Team[] + * @return TeamInfo[] */ public function listByName(string $name): array { - $teams = []; - $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; + return $this->teams->listByName($name); } - public function displayTeam(int $id): Team { - $members = []; - $result = $this->gateway->getTeamById($id); - $resultMembers = $this->gateway->getMembersById($id); - foreach ($resultMembers as $row) { - var_dump($row['role']); - if ($row['role'] == 'C') { - $role = MemberRole::coach(); - } 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); + /** + * @param int $id + * @return Team + */ + public function getTeam(int $id): Team { + $teamInfo = $this->teams->getTeamById($id); + $members = $this->members->getMembersOfTeam($id); + return new Team($teamInfo, $members); } + + /** + * delete a member from given team identifier + * @param string $mail + * @param int $teamId + * @return int + */ public function deleteMember(string $mail, int $teamId): int { - $memberId = $this->gateway->getMemberIdByMail($mail); - $this->gateway->deleteMember($teamId, $memberId); + $userId = $this->users->getAccountFromMail($mail)->getId(); + $this->members->remove($teamId, $userId); return $teamId; } diff --git a/src/Core/Validation/ValidationFail.php b/src/Core/Validation/ValidationFail.php index 01b3473..9a714e5 100644 --- a/src/Core/Validation/ValidationFail.php +++ b/src/Core/Validation/ValidationFail.php @@ -33,10 +33,18 @@ class ValidationFail implements JsonSerializable { 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 { return new ValidationFail("Not found", $message); } + /** + * @param string $message + * @return ValidationFail validation fail for unauthorized accesses + */ public static function unauthorized(string $message = "Unauthorized"): ValidationFail { return new ValidationFail("Unauthorized", $message); } diff --git a/src/Core/Validation/Validators.php b/src/Core/Validation/Validators.php index b64d704..52bc08c 100644 --- a/src/Core/Validation/Validators.php +++ b/src/Core/Validation/Validators.php @@ -12,10 +12,18 @@ class Validators { public static function regex(string $regex, ?string $msg = null): Validator { return new SimpleFunctionValidator( 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 `_`. */ @@ -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 { return self::regex("/^[0-9]+$/"); diff --git a/src/Core/Validator/TacticValidator.php b/src/Core/Validator/TacticValidator.php index 9c5ba11..6c9a57b 100644 --- a/src/Core/Validator/TacticValidator.php +++ b/src/Core/Validator/TacticValidator.php @@ -6,6 +6,12 @@ use IQBall\Core\Data\TacticInfo; use IQBall\Core\Validation\ValidationFail; 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 { if ($tactic == null) { return ValidationFail::notFound("La tactique $tacticId n'existe pas"); @@ -17,5 +23,4 @@ class TacticValidator { return null; } - } From 1bf3dfa3b659c664dc1eef925d0818f119d26a5a Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sun, 26 Nov 2023 00:09:19 +0100 Subject: [PATCH 65/75] add and fix conception --- Documentation/Conception.md | 123 ++++++++++++++++++ public/api/index.php | 26 +++- public/index.php | 10 +- src/Api/API.php | 26 +--- src/Api/Controller/APIAuthController.php | 2 +- src/Api/Controller/APITacticController.php | 2 +- src/App/App.php | 2 +- src/App/Control.php | 16 +-- src/App/Controller/AuthController.php | 5 +- src/App/Controller/EditorController.php | 10 +- src/App/Controller/TeamController.php | 4 +- src/App/Controller/UserController.php | 3 +- src/App/Controller/VisualizerController.php | 8 +- .../Session/MutableSessionHandle.php | 2 +- .../Session/PhpSessionHandle.php | 2 +- src/{Core => App}/Session/SessionHandle.php | 2 +- .../Validator/TacticValidator.php | 13 +- src/Core/Validation/ComposedValidator.php | 5 +- src/Core/Validation/Validator.php | 2 +- src/{utils.php => index-utils.php} | 0 20 files changed, 188 insertions(+), 75 deletions(-) create mode 100644 Documentation/Conception.md rename src/{Core => App}/Session/MutableSessionHandle.php (93%) rename src/{Core => App}/Session/PhpSessionHandle.php (96%) rename src/{Core => App}/Session/SessionHandle.php (94%) rename src/{Core => App}/Validator/TacticValidator.php (59%) rename src/{utils.php => index-utils.php} (100%) diff --git a/Documentation/Conception.md b/Documentation/Conception.md new file mode 100644 index 0000000..68b4cd9 --- /dev/null +++ b/Documentation/Conception.md @@ -0,0 +1,123 @@ +# Conception + +## Organisation + +Notre projet est divisé en plusieurs parties: + +- `src/API`, qui définit les classes qui implémentent les actions de l’api +- `src/App`, qui définit les contrôleurs et les vues de l’application web +- `src/Core`, définit les modèles, les classes métiers, les librairies internes (validation, http), les gateways, en somme, les élements logiques de l’application et les actions que l’ont peut faire avec. +- `sql`, définit la base de donnée utilisée, et éxécute les fichiers sql lorsque la base de donnée n’est pas initialisée. +- `profiles`, définit les profiles d’execution, voir `Documentation/how-to-dev.md` pour plus d’info +- `front` contient le code front-end react/typescript +- `ci` contient les scripts de déploiement et la définition du workflow d’intégration continue et de déploiement constant vers notre staging server ([maxou.dev//public/](https://maxou.dev/IQBall/master/public)). +- `public` point d’entrée, avec : + - `public/index.php` point d’entrée pour la webapp + - `public/api/index.php` point d’entrée pour l’api. + +## Backend + +### Validation et résilience des erreurs +#### Motivation +Un controlleur a pour but de valider les données d'une requête avant de les manipuler. + +Nous nous sommes rendu compte que la vérification des données d'une requête était redondante, même en factorisant les différents +types de validation, nous devions quand même explicitement vérifier la présence des champs utilisés dans la requête. + +```php +public function doPostAction(array $form) { + $failures = []; + $req = new HttpRequest($form); + $email = $req['email'] ?? null; + if ($email == null) { + $failures[] = "Vous n'avez pas entré d'adresse email."; + return; + } + if (Validation::isEmail($email)) { + $failures[] = "Format d'adresse email invalide."; + } + if (Validation::isLenBetween($email, 6, 64))) { + $failures[] = "L'adresse email doit être d'une longueur comprise entre 6 et 64 charactères."; + } + + if (!empty($failures)) { + return ViewHttpResponse::twig('error.html.twig', ['failures' => $failures]); + } + + // traitement ... +} +``` + +Nous sommes obligés de tester à la main la présence des champs dans la requête, et nous avons une paire condition/erreur par type de validation, +ce qui, pour des requêtes avec plusieurs champs, peut vite devenir illisible si nous voulons être précis sur les erreurs. + +Ici, une validation est une règle, un prédicat qui permet de valider une donnée sur un critère bien précis (injection html, adresse mail, longueur, etc.). +Bien souvent, lorsque le prédicat échoue, un message est ajouté à la liste des erreurs rencontrés, mais ce message est souvent le même, ce qui rajoute en plus +de la redondance sur les messages d'erreurs choisis lorsqu'une validation échoue. + +#### Schéma +Toutes ces lignes de code pour définir que notre requête doit contenir un champ nommé `email`, dont la valeur est une adresse mail, d'une longueur comprise entre 6 et 64. +Nous avons donc trouvé l'idée de définir un système nous permettant de déclarer le schéma d'une requête, +et de reporter le plus d'erreurs possibles lorsqu'une requête ne valide pas le schéma : + +```php +public function doPostAction(array $form): HttpResponse { + $failures = []; + $req = HttpRequest::from($form, $failures, [ + 'email' => [Validators::email(), Validators::isLenBetween(6, 64)] + ]); + + if (!empty($failures)) { //ou $req == null + return ViewHttpResponse::twig('error.html.twig', ['failures' => $failures]) + } + + // traitement ... +} +``` +Ce système nous permet de faire de la programmation _déclarative_, nous disons à _quoi_ ressemble la forme de la requête que l'on souhaite, +plustot que de définir _comment_ réagir face à notre requête. +Ce système permet d'être beaucoup plus clair sur la forme attendue d'une requête, et de garder une lisibilité satisfaisante peu importe le nombre de +champs que celle-ci contient. + +Nous pouvons ensuite emballer les erreurs de validation dans des `ValidationFail` et `FieldValidationFail`, ce qui permet ensuite d'obtenir +plus de précision sur une erreur, comme le nom du champ qui est invalidé, et qui permet ensuite à nos vues de pouvoir manipuler plus facilement +les erreurs et facilement entourer les champs invalides en rouge, ainsi que d'afficher toutes les erreurs que l'utilisateur a fait, d'un coup. + +### HttpRequest, HttpResponse +Nous avons choisi d'encapsuler les requêtes et les réponses afin de faciliter leur manipulation. +Cela permet de mieux repérer les endroits du code qui manipulent une requête d'un simple tableau, +et de garder à un seul endroit la responsabilitée d'écrire le contenu de la requête vers client. + +`src/App` définit une `ViewHttpResponse`, qui permet aux controlleurs de retourner la vue qu'ils ont choisit. +C'est ensuite à la classe `src/App/App` d'afficher la réponse. + +### index.php +Il y a deux points d'entrés, celui de la WebApp (`public/index.php`), et celui de l'API (`public/api/index.php`). +Ces fichiers ont pour but de définir ce qui va être utilisé dans l'application, comme les routes, la base de donnée, les classes métiers utilisés, +comment gérer l'authentification dans le cadre de l'API, et une session dans le cadre de la web app. + +L'index définit aussi quoi faire lorsque l'application retourne une réponse. Dans les implémentations actuelles, elle délègue simplement +l'affichage dans une classe (`IQBall\App\App` et `IQBall\API\API`). + +### API +Nous avons définit une petite API (`src/Api`) pour nous permettre de faire des actions en arrière plan depuis le front end. +Par exemple, l'API permet de changer le nom d'une tactique, ou de sauvegarder son contenu. +C'est plus pratique de faire des requêtes en arrière-plan plustot que faire une requête directement à la partie WebApp, ce qui +aurait eu pour conséquences de recharger la page + + +## Frontend + +### Utilisation de React + +Notre application est une application de création et de visualisation de stratégies pour des match de basket. +L’éditeur devra comporter un terrain sur lequel il est possible de placer et bouger des pions, représentant les joueurs. +Une stratégie est un arbre composé de plusieurs étapes, une étape étant constituée d’un ensemble de joueurs et d’adversaires sur le terrain, +aillant leur position de départ, et leur position cible, avec des flèches représentant le type de mouvement (dribble, écran, etc) effectué. +les enfants d’une étape, sont d’autres étapes en fonction des cas de figures (si tel joueur fait tel mouvement, ou si tel joueur fait telle passe etc). +Pour rendre le tout agréable à utiliser, il faut que l’interface soit réactive : si l’on bouge un joueur, +il faut que les flèches qui y sont liés bougent aussi, il faut que les joueurs puissent bouger le long des flèches en mode visualiseur etc… + +Le front-end de l’éditeur et du visualiseur étant assez ambitieux, et occupant une place importante du projet, nous avons décidés de l’effectuer en utilisant +le framework React qui rend simple le développement d’interfaces dynamiques, et d’utiliser typescript parce qu’ici on code bien et qu’on impose une type safety a notre code. + diff --git a/public/api/index.php b/public/api/index.php index 076dd11..6f46638 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -3,11 +3,12 @@ require "../../config.php"; require "../../vendor/autoload.php"; require "../../sql/database.php"; -require "../utils.php"; +require "../../src/index-utils.php"; use IQBall\Api\API; use IQBall\Api\Controller\APIAuthController; use IQBall\Api\Controller\APITacticController; +use IQBall\App\Session\PhpSessionHandle; use IQBall\Core\Action; use IQBall\Core\Connection; use IQBall\Core\Data\Account; @@ -34,4 +35,25 @@ function getRoutes(): AltoRouter { return $router; } -Api::render(API::handleMatch(getRoutes()->match())); +/** + * Defines the way of being authorised through the API + * By checking if an Authorisation header is set, and by expecting its value to be a valid token of an account. + * If the header is not set, fallback to the App's PHP session system, and try to extract the account from it. + * @return Account|null + * @throws Exception + */ +function tryGetAuthorization(): ?Account { + $headers = getallheaders(); + + // If no authorization header is set, try fallback to php session. + if (!isset($headers['Authorization'])) { + $session = PhpSessionHandle::init(); + return $session->getAccount(); + } + + $token = $headers['Authorization']; + $gateway = new AccountGateway(new Connection(get_database())); + return $gateway->getAccountFromToken($token); +} + +Api::render(API::handleMatch(getRoutes()->match(), fn() => tryGetAuthorization())); diff --git a/public/index.php b/public/index.php index 807ec88..01ba967 100644 --- a/public/index.php +++ b/public/index.php @@ -4,8 +4,8 @@ require "../vendor/autoload.php"; require "../config.php"; require "../sql/database.php"; -require "../src/utils.php"; require "../src/App/react-display.php"; +require "../src/index-utils.php"; use IQBall\App\App; use IQBall\App\Controller\AuthController; @@ -13,6 +13,9 @@ use IQBall\App\Controller\EditorController; use IQBall\App\Controller\TeamController; use IQBall\App\Controller\UserController; use IQBall\App\Controller\VisualizerController; +use IQBall\App\Session\MutableSessionHandle; +use IQBall\App\Session\PhpSessionHandle; +use IQBall\App\Session\SessionHandle; use IQBall\App\ViewHttpResponse; use IQBall\Core\Action; use IQBall\Core\Connection; @@ -25,9 +28,6 @@ use IQBall\Core\Http\HttpResponse; use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TeamModel; -use IQBall\Core\Session\MutableSessionHandle; -use IQBall\Core\Session\PhpSessionHandle; -use IQBall\Core\Session\SessionHandle; use IQBall\Core\Validation\ValidationFail; function getConnection(): Connection { @@ -68,8 +68,8 @@ function getRoutes(): AltoRouter { $ar->map("POST", "/register", Action::noAuth(fn(SessionHandle $s) => getAuthController()->register($_POST, $s))); //user-related - $ar->map("GET", "/home", Action::auth(fn(SessionHandle $s) => getUserController()->home($s))); $ar->map("GET", "/", Action::auth(fn(SessionHandle $s) => getUserController()->home($s))); + $ar->map("GET", "/home", Action::auth(fn(SessionHandle $s) => getUserController()->home($s))); $ar->map("GET", "/settings", Action::auth(fn(SessionHandle $s) => getUserController()->settings($s))); //tactic-related diff --git a/src/Api/API.php b/src/Api/API.php index d266e79..e96b6b6 100644 --- a/src/Api/API.php +++ b/src/Api/API.php @@ -4,12 +4,8 @@ namespace IQBall\Api; use Exception; use IQBall\Core\Action; -use IQBall\Core\Connection; -use IQBall\Core\Data\Account; -use IQBall\Core\Gateway\AccountGateway; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; -use IQBall\Core\Session\PhpSessionHandle; use IQBall\Core\Validation\ValidationFail; class API { @@ -28,12 +24,14 @@ class API { } } + /** - * @param mixed[] $match + * @param array $match + * @param callable $tryGetAuthorization function to return an authorisation object for the given action (if required) * @return HttpResponse * @throws Exception */ - public static function handleMatch(array $match): HttpResponse { + public static function handleMatch(array $match, callable $tryGetAuthorization): HttpResponse { if (!$match) { return new JsonHttpResponse([ValidationFail::notFound("not found")]); } @@ -46,7 +44,7 @@ class API { $auth = null; if ($action->isAuthRequired()) { - $auth = self::tryGetAuthorization(); + $auth = call_user_func($tryGetAuthorization); if ($auth == null) { return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")]); } @@ -54,18 +52,4 @@ class API { return $action->run($match['params'], $auth); } - - private static function tryGetAuthorization(): ?Account { - $headers = getallheaders(); - - // If no authorization header is set, try fallback to php session. - if (!isset($headers['Authorization'])) { - $session = PhpSessionHandle::init(); - return $session->getAccount(); - } - - $token = $headers['Authorization']; - $gateway = new AccountGateway(new Connection(get_database())); - return $gateway->getAccountFromToken($token); - } } diff --git a/src/Api/Controller/APIAuthController.php b/src/Api/Controller/APIAuthController.php index 332f260..5e149a5 100644 --- a/src/Api/Controller/APIAuthController.php +++ b/src/Api/Controller/APIAuthController.php @@ -38,7 +38,7 @@ class APIAuthController { } return new JsonHttpResponse(["authorization" => $account->getToken()]); - }, true); + }); } } diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 63e4811..04f0a50 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -43,6 +43,6 @@ class APITacticController { } return HttpResponse::fromCode(HttpCodes::OK); - }, true); + }); } } diff --git a/src/App/App.php b/src/App/App.php index 7fa2595..de288e8 100644 --- a/src/App/App.php +++ b/src/App/App.php @@ -2,10 +2,10 @@ namespace IQBall\App; +use IQBall\App\Session\MutableSessionHandle; use IQBall\Core\Action; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; -use IQBall\Core\Session\MutableSessionHandle; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; diff --git a/src/App/Control.php b/src/App/Control.php index 3dea9b1..f3860ec 100644 --- a/src/App/Control.php +++ b/src/App/Control.php @@ -16,22 +16,17 @@ class Control { * @param array $schema an array of `fieldName => Validators` which represents the request object schema * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. * THe callback must accept an HttpRequest, and return an HttpResponse object. - * @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead - * of the ViewHttpResponse for an error view. * @return HttpResponse */ - public static function runChecked(array $schema, callable $run, bool $errorInJson): HttpResponse { + public static function runChecked(array $schema, callable $run): HttpResponse { $request_body = file_get_contents('php://input'); $payload_obj = json_decode($request_body); if (!$payload_obj instanceof \stdClass) { $fail = new ValidationFail("bad-payload", "request body is not a valid json object"); - if ($errorInJson) { - return new JsonHttpResponse([$fail, HttpCodes::BAD_REQUEST]); - } return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST); } $payload = get_object_vars($payload_obj); - return self::runCheckedFrom($payload, $schema, $run, $errorInJson); + return self::runCheckedFrom($payload, $schema, $run); } /** @@ -40,18 +35,13 @@ class Control { * @param array $schema an array of `fieldName => Validators` which represents the request object schema * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. * THe callback must accept an HttpRequest, and return an HttpResponse object. - * @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead - * of the ViewHttpResponse for an error view. * @return HttpResponse */ - public static function runCheckedFrom(array $data, array $schema, callable $run, bool $errorInJson): HttpResponse { + public static function runCheckedFrom(array $data, array $schema, callable $run): HttpResponse { $fails = []; $request = HttpRequest::from($data, $fails, $schema); if (!empty($fails)) { - if ($errorInJson) { - return new JsonHttpResponse($fails, HttpCodes::BAD_REQUEST); - } return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST); } diff --git a/src/App/Controller/AuthController.php b/src/App/Controller/AuthController.php index 9e790e9..dfc5f52 100644 --- a/src/App/Controller/AuthController.php +++ b/src/App/Controller/AuthController.php @@ -2,12 +2,11 @@ namespace IQBall\App\Controller; +use IQBall\App\Session\MutableSessionHandle; +use IQBall\App\ViewHttpResponse; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; -use IQBall\App\ViewHttpResponse; use IQBall\Core\Model\AuthModel; -use IQBall\Core\Session\MutableSessionHandle; -use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\Validators; class AuthController { diff --git a/src/App/Controller/EditorController.php b/src/App/Controller/EditorController.php index 6724ced..bba3214 100644 --- a/src/App/Controller/EditorController.php +++ b/src/App/Controller/EditorController.php @@ -2,13 +2,13 @@ namespace IQBall\App\Controller; +use IQBall\App\Session\SessionHandle; +use IQBall\App\Validator\TacticValidator; +use IQBall\App\ViewHttpResponse; use IQBall\Core\Data\TacticInfo; use IQBall\Core\Http\HttpCodes; -use IQBall\Core\Http\HttpResponse; -use IQBall\App\ViewHttpResponse; use IQBall\Core\Model\TacticModel; -use IQBall\Core\Session\SessionHandle; -use IQBall\Core\Validator\TacticValidator; +use IQBall\Core\Validation\ValidationFail; class EditorController { private TacticModel $model; @@ -44,7 +44,7 @@ class EditorController { public function openEditor(int $id, SessionHandle $session): ViewHttpResponse { $tactic = $this->model->get($id); - $failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId()); + $failure = TacticValidator::validateAccess($id, $tactic, $session->getAccount()->getId()); if ($failure != null) { return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); diff --git a/src/App/Controller/TeamController.php b/src/App/Controller/TeamController.php index deaf61b..b2c0ea9 100644 --- a/src/App/Controller/TeamController.php +++ b/src/App/Controller/TeamController.php @@ -2,11 +2,11 @@ namespace IQBall\App\Controller; +use IQBall\App\Session\SessionHandle; +use IQBall\App\ViewHttpResponse; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; -use IQBall\App\ViewHttpResponse; use IQBall\Core\Model\TeamModel; -use IQBall\Core\Session\SessionHandle; use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\Validators; diff --git a/src/App/Controller/UserController.php b/src/App/Controller/UserController.php index b7cef0b..d6f9f89 100644 --- a/src/App/Controller/UserController.php +++ b/src/App/Controller/UserController.php @@ -2,10 +2,9 @@ namespace IQBall\App\Controller; -use IQBall\Core\Http\HttpResponse; +use IQBall\App\Session\SessionHandle; use IQBall\App\ViewHttpResponse; use IQBall\Core\Model\TacticModel; -use IQBall\Core\Session\SessionHandle; class UserController { private TacticModel $tactics; diff --git a/src/App/Controller/VisualizerController.php b/src/App/Controller/VisualizerController.php index 48e1168..271c4e9 100644 --- a/src/App/Controller/VisualizerController.php +++ b/src/App/Controller/VisualizerController.php @@ -2,12 +2,12 @@ namespace IQBall\App\Controller; +use IQBall\App\Session\SessionHandle; +use IQBall\App\Validator\TacticValidator; +use IQBall\App\ViewHttpResponse; use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpResponse; -use IQBall\App\ViewHttpResponse; use IQBall\Core\Model\TacticModel; -use IQBall\Core\Session\SessionHandle; -use IQBall\Core\Validator\TacticValidator; class VisualizerController { private TacticModel $tacticModel; @@ -28,7 +28,7 @@ class VisualizerController { public function openVisualizer(int $id, SessionHandle $session): HttpResponse { $tactic = $this->tacticModel->get($id); - $failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId()); + $failure = TacticValidator::validateAccess($id, $tactic, $session->getAccount()->getId()); if ($failure != null) { return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); diff --git a/src/Core/Session/MutableSessionHandle.php b/src/App/Session/MutableSessionHandle.php similarity index 93% rename from src/Core/Session/MutableSessionHandle.php rename to src/App/Session/MutableSessionHandle.php index 1d7ae86..9ef23c0 100644 --- a/src/Core/Session/MutableSessionHandle.php +++ b/src/App/Session/MutableSessionHandle.php @@ -1,6 +1,6 @@ getOwnerId() != $ownerId) { return ValidationFail::unauthorized("Vous ne pouvez pas accéder à cette tactique."); } - return null; } + } diff --git a/src/Core/Validation/ComposedValidator.php b/src/Core/Validation/ComposedValidator.php index fcf81d9..58f4910 100644 --- a/src/Core/Validation/ComposedValidator.php +++ b/src/Core/Validation/ComposedValidator.php @@ -17,7 +17,10 @@ class ComposedValidator extends Validator { public function validate(string $name, $val): array { $firstFailures = $this->first->validate($name, $val); - $thenFailures = $this->then->validate($name, $val); + $thenFailures = []; + if (empty($firstFailures)) { + $thenFailures = $this->then->validate($name, $val); + } return array_merge($firstFailures, $thenFailures); } } diff --git a/src/Core/Validation/Validator.php b/src/Core/Validation/Validator.php index 8624bfe..d1761da 100644 --- a/src/Core/Validation/Validator.php +++ b/src/Core/Validation/Validator.php @@ -13,7 +13,7 @@ abstract class Validator { /** * Creates a validator composed of this validator, and given validator - * @param Validator $other the second validator to chain with + * @param Validator $other the second validator to validate if this validator succeeded * @return Validator a composed validator */ public function then(Validator $other): Validator { diff --git a/src/utils.php b/src/index-utils.php similarity index 100% rename from src/utils.php rename to src/index-utils.php From d1564a3549f5845c464fe5b82baef2f9de9156a2 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sun, 26 Nov 2023 14:00:30 +0100 Subject: [PATCH 66/75] add a gest mode for editor if user is not connected --- front/components/editor/SavingState.tsx | 4 ++ front/style/editor.css | 3 +- front/views/Editor.tsx | 72 +++++++++++++-------- public/index.php | 4 +- src/Api/API.php | 2 +- src/App/Controller/EditorController.php | 22 ++++++- src/App/Controller/VisualizerController.php | 2 +- src/Core/Gateway/TacticInfoGateway.php | 1 - 8 files changed, 74 insertions(+), 36 deletions(-) diff --git a/front/components/editor/SavingState.tsx b/front/components/editor/SavingState.tsx index df03628..68c2285 100644 --- a/front/components/editor/SavingState.tsx +++ b/front/components/editor/SavingState.tsx @@ -4,6 +4,10 @@ export interface SaveState { } export class SaveStates { + static readonly Guest: SaveState = { + className: "save-state-guest", + message: "you are not connected, your changes will not be saved.", + } static readonly Ok: SaveState = { className: "save-state-ok", message: "saved", diff --git a/front/style/editor.css b/front/style/editor.css index d832fc3..eefa561 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -85,6 +85,7 @@ color: green; } -.save-state-saving { +.save-state-saving, +.save-state-guest { color: gray; } diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index dddf5cc..13f7684 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -29,7 +29,7 @@ const ERROR_STYLE: CSSProperties = { export interface EditorViewProps { tactic: Tactic - onContentChange: (tactic: TacticContent) => Promise + onContentChange: (tactic: TacticContent) => Promise onNameChange: (name: string) => Promise } @@ -50,31 +50,43 @@ export default function Editor({ name: string content: string }) { + const isInGuestMode = id == -1 + return ( - fetchAPI(`tactic/${id}/save`, { content }).then((r) => r.ok) - } - onNameChange={(name: string) => - fetchAPI(`tactic/${id}/edit/name`, { name }).then((r) => r.ok) - } + onContentChange={async (content: TacticContent) => { + if (isInGuestMode) { + return SaveStates.Guest + } + return fetchAPI(`tactic/${id}/save`, { content }).then((r) => + r.ok ? SaveStates.Ok : SaveStates.Err, + ) + }} + onNameChange={async (name: string) => { + if (isInGuestMode) { + return true //simulate that the name has been changed + } + return fetchAPI(`tactic/${id}/edit/name`, { name }).then( + (r) => r.ok, + ) + }} /> ) } function EditorView({ - tactic: { name, content: initialContent }, + tactic: { id, name, content: initialContent }, onContentChange, onNameChange, }: EditorViewProps) { + const isInGuestMode = id == -1 + const [style, setStyle] = useState({}) const [content, setContent, saveState] = useContentState( initialContent, - (content) => - onContentChange(content).then((success) => - success ? SaveStates.Ok : SaveStates.Err, - ), + isInGuestMode ? SaveStates.Guest : SaveStates.Ok, + onContentChange, ) const [allies, setAllies] = useState( getRackPlayers(Team.Allies, content.players), @@ -220,26 +232,30 @@ function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] { function useContentState( initialContent: S, + initialSaveState: SaveState, saveStateCallback: (s: S) => Promise, ): [S, Dispatch>, SaveState] { const [content, setContent] = useState(initialContent) - const [savingState, setSavingState] = useState(SaveStates.Ok) + const [savingState, setSavingState] = useState(initialSaveState) - const setContentSynced = useCallback((newState: SetStateAction) => { - setContent((content) => { - const state = - typeof newState === "function" - ? (newState as (state: S) => S)(content) - : newState - if (state !== content) { - setSavingState(SaveStates.Saving) - saveStateCallback(state) - .then(setSavingState) - .catch(() => setSavingState(SaveStates.Err)) - } - return state - }) - }, [saveStateCallback]) + const setContentSynced = useCallback( + (newState: SetStateAction) => { + setContent((content) => { + const state = + typeof newState === "function" + ? (newState as (state: S) => S)(content) + : newState + if (state !== content) { + setSavingState(SaveStates.Saving) + saveStateCallback(state) + .then(setSavingState) + .catch(() => setSavingState(SaveStates.Err)) + } + return state + }) + }, + [saveStateCallback], + ) return [content, setContentSynced, savingState] } diff --git a/public/index.php b/public/index.php index c31e289..78ee4d6 100644 --- a/public/index.php +++ b/public/index.php @@ -88,7 +88,9 @@ function getRoutes(): AltoRouter { //tactic-related $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()->openEditor($id, $s))); - $ar->map("GET", "/tactic/new", Action::auth(fn(SessionHandle $s) => getEditorController()->createNew($s))); + // don't require an authentication to run this action. + // If the user is not connected, the tactic will never save. + $ar->map("GET", "/tactic/new", Action::noAuth(fn(SessionHandle $s) => getEditorController()->createNew($s))); //team-related $ar->map("GET", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->displayCreateTeam($s))); diff --git a/src/Api/API.php b/src/Api/API.php index f79e1b5..da00749 100644 --- a/src/Api/API.php +++ b/src/Api/API.php @@ -19,7 +19,7 @@ class API { if ($response instanceof JsonHttpResponse) { header('Content-type: application/json'); echo $response->getJson(); - } else if (get_class($response) != HttpResponse::class) { + } elseif (get_class($response) != HttpResponse::class) { throw new Exception("API returned unknown Http Response"); } } diff --git a/src/App/Controller/EditorController.php b/src/App/Controller/EditorController.php index d2a2bc4..3994093 100644 --- a/src/App/Controller/EditorController.php +++ b/src/App/Controller/EditorController.php @@ -28,13 +28,31 @@ class EditorController { ]); } + /** + * @return ViewHttpResponse the editor view for a test tactic. + */ + private function openTestEditor(): ViewHttpResponse { + return ViewHttpResponse::react("views/Editor.tsx", [ + "id" => -1, //-1 id means that the editor will not support saves + "content" => '{"players": []}', + "name" => TacticModel::TACTIC_DEFAULT_NAME, + ]); + } + /** * creates a new empty tactic, with default name + * If the given session does not contain a connected account, + * open a test editor. * @param SessionHandle $session * @return ViewHttpResponse the editor view */ public function createNew(SessionHandle $session): ViewHttpResponse { - $tactic = $this->model->makeNewDefault($session->getAccount()->getId()); + $account = $session->getAccount(); + + if ($account == null) { + return $this->openTestEditor(); + } + $tactic = $this->model->makeNewDefault($account->getId()); return $this->openEditorFor($tactic); } @@ -55,6 +73,4 @@ class EditorController { return $this->openEditorFor($tactic); } - - } diff --git a/src/App/Controller/VisualizerController.php b/src/App/Controller/VisualizerController.php index 271c4e9..631468e 100644 --- a/src/App/Controller/VisualizerController.php +++ b/src/App/Controller/VisualizerController.php @@ -20,7 +20,7 @@ class VisualizerController { } /** - * opens a visualisation page for the tactic specified by its identifier in the url. + * Opens a visualisation page for the tactic specified by its identifier in the url. * @param int $id * @param SessionHandle $session * @return HttpResponse diff --git a/src/Core/Gateway/TacticInfoGateway.php b/src/Core/Gateway/TacticInfoGateway.php index 6b66f2c..447c7a5 100644 --- a/src/Core/Gateway/TacticInfoGateway.php +++ b/src/Core/Gateway/TacticInfoGateway.php @@ -99,5 +99,4 @@ class TacticInfoGateway { ]); return $stmnt->rowCount() == 1; } - } From 44513a50499b22337f2b51f898ebe072e1058fa1 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sun, 26 Nov 2023 14:09:02 +0100 Subject: [PATCH 67/75] fix player keys in BasketCourt --- front/components/editor/BasketCourt.tsx | 2 +- front/components/editor/CourtPlayer.tsx | 1 - front/tactic/Player.ts | 6 ------ front/views/Editor.tsx | 15 +++++++-------- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 92722c7..115c41b 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -23,7 +23,7 @@ export function BasketCourt({ {players.map((player) => { return ( onPlayerRemove(player)} diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index 9f0c9e4..22e1bbe 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -43,7 +43,6 @@ export default function CourtPlayer({ setY(y) onChange({ - id: player.id, rightRatio: x, bottomRatio: y, team: player.team, diff --git a/front/tactic/Player.ts b/front/tactic/Player.ts index e7553a8..6530612 100644 --- a/front/tactic/Player.ts +++ b/front/tactic/Player.ts @@ -1,12 +1,6 @@ import { Team } from "./Team" export interface Player { - /** - * unique identifier of the player. - * This identifier must be unique to the associated court. - */ - id: number - /** * the player's team * */ diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 8f6483c..df566ff 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -128,7 +128,6 @@ function EditorView({ return [ ...players, { - id: players.length, team: element.team, role: element.key, rightRatio: x, @@ -151,11 +150,7 @@ function EditorView({ default_value={name} on_validated={(new_name) => { onNameChange(new_name).then((success) => { - if (success) { - setStyle({}) - } else { - setStyle(ERROR_STYLE) - } + success ? setStyle({}) : setStyle(ERROR_STYLE) }) }} /> @@ -192,7 +187,9 @@ function EditorView({ onPlayerChange={(player) => { setPlayers((players) => { const idx = players.findIndex( - (p) => p.id === player.id, + (p) => + p.team === player.team && + p.role === player.role, ) return players.toSpliced(idx, 1, player) }) @@ -200,7 +197,9 @@ function EditorView({ onPlayerRemove={(player) => { setPlayers((players) => { const idx = players.findIndex( - (p) => p.id === player.id, + (p) => + p.team === player.team && + p.role === player.role, ) return players.toSpliced(idx, 1) }) From 1147ad114f78a23b73f062acd26d6a59deebb214 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sun, 26 Nov 2023 14:23:50 +0100 Subject: [PATCH 68/75] fix routes in views by adding global variable basePath in twig --- public/api/index.php | 2 +- public/index.php | 16 ++- src/Api/API.php | 2 - src/Api/Controller/APIAuthController.php | 2 +- src/Api/Controller/APITacticController.php | 2 +- src/App/App.php | 15 +-- src/App/Control.php | 4 +- src/App/Views/account_settings.twig | 35 +++-- src/App/Views/add_member.html.twig | 8 +- src/App/Views/delete_member.html.twig | 2 +- src/App/Views/display_login.html.twig | 19 ++- src/App/Views/display_register.html.twig | 20 +-- src/App/Views/display_team.html.twig | 144 +++++++++++---------- src/App/Views/display_teams.html.twig | 4 +- src/App/Views/error.html.twig | 92 ++++++------- src/App/Views/home.twig | 100 +++++++------- src/App/Views/insert_team.html.twig | 11 +- src/App/Views/list_team_by_name.html.twig | 7 +- src/index-utils.php | 4 +- 19 files changed, 253 insertions(+), 236 deletions(-) diff --git a/public/api/index.php b/public/api/index.php index 6f46638..fd39bfa 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -27,7 +27,7 @@ function getAuthController(): APIAuthController { function getRoutes(): AltoRouter { $router = new AltoRouter(); - $router->setBasePath(get_public_path() . "/api"); + $router->setBasePath(get_public_path(__DIR__)); $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); $router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize())); diff --git a/public/index.php b/public/index.php index 01ba967..8b4acf7 100644 --- a/public/index.php +++ b/public/index.php @@ -29,6 +29,8 @@ use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TeamModel; use IQBall\Core\Validation\ValidationFail; +use Twig\Environment; +use Twig\Loader\FilesystemLoader; function getConnection(): Connection { return new Connection(get_database()); @@ -55,6 +57,16 @@ function getAuthController(): AuthController { return new AuthController(new AuthModel(new AccountGateway(getConnection()))); } +function getTwig(): Environment { + global $basePath; + $fl = new FilesystemLoader("../src/App/Views"); + $twig = new Environment($fl); + + $twig->addGlobal("basePath", $basePath); + + return $twig; +} + function getRoutes(): AltoRouter { global $basePath; @@ -104,6 +116,6 @@ function runMatch($match, MutableSessionHandle $session): HttpResponse { //this is a global variable -$basePath = get_public_path(); +$basePath = get_public_path(__DIR__); -App::render(runMatch(getRoutes()->match(), PhpSessionHandle::init()), "../src/App/Views/"); +App::render(runMatch(getRoutes()->match(), PhpSessionHandle::init()), fn() => getTwig()); diff --git a/src/Api/API.php b/src/Api/API.php index e96b6b6..03daf91 100644 --- a/src/Api/API.php +++ b/src/Api/API.php @@ -19,8 +19,6 @@ class API { if ($response instanceof JsonHttpResponse) { header('Content-type: application/json'); echo $response->getJson(); - } else { - throw new Exception("API returned a non-json response."); } } diff --git a/src/Api/Controller/APIAuthController.php b/src/Api/Controller/APIAuthController.php index 5e149a5..fc0eef6 100644 --- a/src/Api/Controller/APIAuthController.php +++ b/src/Api/Controller/APIAuthController.php @@ -2,8 +2,8 @@ namespace IQBall\Api\Controller; +use IQBall\App\Control; use IQBall\Core\Http\HttpCodes; -use IQBall\Core\Route\Control; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 04f0a50..e8a1731 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -2,7 +2,7 @@ namespace IQBall\Api\Controller; -use IQBall\Core\Route\Control; +use IQBall\App\Control; use IQBall\Core\Data\Account; use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpRequest; diff --git a/src/App/App.php b/src/App/App.php index de288e8..cd3c293 100644 --- a/src/App/App.php +++ b/src/App/App.php @@ -16,13 +16,13 @@ class App { /** * renders (prints out) given HttpResponse to the client * @param HttpResponse $response - * @param string $twigViewsFolder + * @param callable(): Environment $twigSupplier * @return void * @throws LoaderError * @throws RuntimeError * @throws SyntaxError */ - public static function render(HttpResponse $response, string $twigViewsFolder): void { + public static function render(HttpResponse $response, callable $twigSupplier): void { http_response_code($response->getCode()); foreach ($response->getHeaders() as $header => $value) { @@ -30,7 +30,7 @@ class App { } if ($response instanceof ViewHttpResponse) { - self::renderView($response, $twigViewsFolder); + self::renderView($response, $twigSupplier); } elseif ($response instanceof JsonHttpResponse) { header('Content-type: application/json'); echo $response->getJson(); @@ -40,13 +40,13 @@ class App { /** * renders (prints out) given ViewHttpResponse to the client * @param ViewHttpResponse $response - * @param string $twigViewsFolder + * @param callable(): Environment $twigSupplier * @return void * @throws LoaderError * @throws RuntimeError * @throws SyntaxError */ - private static function renderView(ViewHttpResponse $response, string $twigViewsFolder): void { + private static function renderView(ViewHttpResponse $response, callable $twigSupplier): void { $file = $response->getFile(); $args = $response->getArguments(); @@ -56,10 +56,9 @@ class App { break; case ViewHttpResponse::TWIG_VIEW: try { - $fl = new FilesystemLoader($twigViewsFolder); - $twig = new Environment($fl); + $twig = call_user_func($twigSupplier); $twig->display($file, $args); - } catch (RuntimeError | SyntaxError | LoaderError $e) { + } catch (RuntimeError|SyntaxError|LoaderError $e) { http_response_code(500); echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); throw $e; diff --git a/src/App/Control.php b/src/App/Control.php index f3860ec..5c2fe0f 100644 --- a/src/App/Control.php +++ b/src/App/Control.php @@ -1,12 +1,10 @@ - - - - - Paramètres + + + + + Paramètres - - + + - - - -

Paramètres

- + + +

Paramètres

+ \ No newline at end of file diff --git a/src/App/Views/add_member.html.twig b/src/App/Views/add_member.html.twig index 6c5a3e3..e083a63 100644 --- a/src/App/Views/add_member.html.twig +++ b/src/App/Views/add_member.html.twig @@ -59,11 +59,11 @@ background-color: #0056b3; } - .role{ + .role { margin-top: 10px; } - .radio{ + .radio { display: flex; justify-content: space-between; } @@ -73,7 +73,7 @@

Ajouter un membre à votre équipe

- +
@@ -81,7 +81,7 @@
- Rôle du membre dans l'équipe : + Rôle du membre dans l'équipe :
diff --git a/src/App/Views/delete_member.html.twig b/src/App/Views/delete_member.html.twig index b7d0d3b..5b161e3 100644 --- a/src/App/Views/delete_member.html.twig +++ b/src/App/Views/delete_member.html.twig @@ -56,7 +56,7 @@

Supprimez un membre de votre équipe

- +
diff --git a/src/App/Views/display_login.html.twig b/src/App/Views/display_login.html.twig index ca6890d..48842c3 100644 --- a/src/App/Views/display_login.html.twig +++ b/src/App/Views/display_login.html.twig @@ -53,30 +53,35 @@ background-color: #0056b3; } - .error-messages{ - color : #ff331a; + .error-messages { + color: #ff331a; font-style: italic; } {% for err in fails %} - .form-group #{{ err.getFieldName() }} { - border-color: red; + .form-group + + # + {{ err.getFieldName() }} + { + border-color: red + ; } {% endfor %}

Se connecter

- +
{% for name in fails %} - + {% endfor %} - +
diff --git a/src/App/Views/display_register.html.twig b/src/App/Views/display_register.html.twig index 2b24e23..d83ec9e 100644 --- a/src/App/Views/display_register.html.twig +++ b/src/App/Views/display_register.html.twig @@ -49,8 +49,8 @@ cursor: pointer; } - .error-messages{ - color : #ff331a; + .error-messages { + color: #ff331a; font-style: italic; } @@ -59,27 +59,31 @@ } {% for err in fails %} - .form-group #{{ err.getFieldName() }} { - border-color: red; + .form-group + + # + {{ err.getFieldName() }} + { + border-color: red + ; } {% endfor %} -

S'enregistrer

- +
{% for name in fails %} - + {% endfor %} - + diff --git a/src/App/Views/display_team.html.twig b/src/App/Views/display_team.html.twig index 6f0aec1..06d64fb 100644 --- a/src/App/Views/display_team.html.twig +++ b/src/App/Views/display_team.html.twig @@ -1,85 +1,91 @@ - - - Twig view - - - -
-

IQBall

-
+ .color { + flex-direction: row; + justify-content: space-between; + } -
+ .logo { + height: 80px; + width: 80px; + } -
-
-

{{ team.getInfo().getName() }}

- -
-
-

Couleur principale :

-

Couleur secondaire :

-
+ + + +
+

IQBall

+
- {% for m in team.listMembers() %} -

{{ m.getUserId() }}

- {% if m.getRole().isCoach() %} -

: Coach

- {% else %} -

: Joueur

- {% endif %} - {% endfor %} +
+ +
+
+

{{ team.getInfo().getName() }}

+ +
+
+

Couleur principale :

+
+
+

Couleur secondaire :

+
+
+ + {% for m in team.listMembers() %} +

{{ m.getUserId() }}

+ {% if m.getRole().isCoach() %} +

: Coach

+ {% else %} +

: Joueur

+ {% endif %} + {% endfor %} +
-
- +
+ \ No newline at end of file diff --git a/src/App/Views/display_teams.html.twig b/src/App/Views/display_teams.html.twig index e4da64e..be9353d 100644 --- a/src/App/Views/display_teams.html.twig +++ b/src/App/Views/display_teams.html.twig @@ -10,7 +10,7 @@

Aucune équipe n'a été trouvée

Chercher une équipe

- +
@@ -22,7 +22,7 @@
{% else %} {% for t in teams %} -
+

Nom de l'équipe : {{ t.name }}

logo de l'équipe
diff --git a/src/App/Views/error.html.twig b/src/App/Views/error.html.twig index e30c2fa..7be397a 100644 --- a/src/App/Views/error.html.twig +++ b/src/App/Views/error.html.twig @@ -1,57 +1,57 @@ - - - Error - - + + + Error + +

IQBall

- {% for fail in failures %} -

{{ fail.getKind() }} : {{ fail.getMessage() }}

- {% endfor %} +{% for fail in failures %} +

{{ fail.getKind() }} : {{ fail.getMessage() }}

+{% endfor %} - + \ No newline at end of file diff --git a/src/App/Views/home.twig b/src/App/Views/home.twig index acf6f6b..5515ac9 100644 --- a/src/App/Views/home.twig +++ b/src/App/Views/home.twig @@ -14,81 +14,83 @@ } #bandeau { - display : flex; - flex-direction : row; + display: flex; + flex-direction: row; } - #bandeau > h1 { - self-align : center; - padding : 0%; - margin : 0%; - justify-content : center; + #bandeau > h1 { + self-align: center; + padding: 0%; + margin: 0%; + justify-content: center; } - + #account { - display : flex; - flex-direction : column; - align-content : center; + display: flex; + flex-direction: column; + align-content: center; } #account:hover { - background-color : gray; + background-color: gray; } #account img { - width : 70%; - height : auto; - align-self : center; - padding : 5%; - margin : 0%; + width: 70%; + height: auto; + align-self: center; + padding: 5%; + margin: 0%; } #account p { - align-self : center; + align-self: center; } -
-

IQ Ball

-
- +

IQ Ball

+
+ Account logo -

Mon profil

-

+ /> +

Mon profil +

+
-

Mes équipes

+

Mes équipes

- + - {% if recentTeam != null %} - {% for team in recentTeam %} -
-

{{team.name}}

-
- {% endfor %} - {% else %} -

Aucune équipe créé !

- {% endif %} +{% if recentTeam != null %} + {% for team in recentTeam %} +
+

{{ team.name }}

+
+ {% endfor %} +{% else %} +

Aucune équipe créé !

+{% endif %} -

Mes strategies

+

Mes strategies

- + - {% if recentTactic != null %} - {% for tactic in recentTactic %} -
-

{{tactic.id}} - {{tactic.name}} - {{tactic.creation_date}}

- -
- {% endfor %} - {% else %} -

Aucune tactique créé !

- {% endif %} +{% if recentTactic != null %} + {% for tactic in recentTactic %} +
+

{{ tactic.id }} - {{ tactic.name }} - {{ tactic.creation_date }}

+ +
+ {% endfor %} +{% else %} +

Aucune tactique créé !

+{% endif %} \ No newline at end of file diff --git a/src/App/Views/insert_team.html.twig b/src/App/Views/insert_team.html.twig index 0eafb55..8174fe6 100644 --- a/src/App/Views/insert_team.html.twig +++ b/src/App/Views/insert_team.html.twig @@ -34,10 +34,7 @@ {% for item in bad_fields %} #{{ item }}{ border-color: red; - } - {% endfor %} - - input[type="text"], input[type="password"] { + }{% endfor %} input[type="text"], input[type="password"] { width: 100%; padding: 10px; border: 1px solid #ccc; @@ -64,12 +61,12 @@

Créer une équipe

- +
- - + + diff --git a/src/App/Views/list_team_by_name.html.twig b/src/App/Views/list_team_by_name.html.twig index 09768e6..858f4c6 100644 --- a/src/App/Views/list_team_by_name.html.twig +++ b/src/App/Views/list_team_by_name.html.twig @@ -34,10 +34,7 @@ {% for item in bad_fields %} #{{ item }}{ border-color: red; - } - {% endfor %} - - input[type="text"], input[type="password"] { + }{% endfor %} input[type="text"], input[type="password"] { width: 100%; padding: 10px; border: 1px solid #ccc; @@ -62,7 +59,7 @@

Chercher une équipe

- +
diff --git a/src/index-utils.php b/src/index-utils.php index 7151386..eb600bc 100644 --- a/src/index-utils.php +++ b/src/index-utils.php @@ -3,9 +3,9 @@ /** * relative path of the public directory from the server's document root. */ -function get_public_path(): string { +function get_public_path(string $public_dir): string { // find the server path of the index.php file - $basePath = substr(__DIR__, strlen($_SERVER['DOCUMENT_ROOT'])); + $basePath = substr($public_dir, strlen($_SERVER['DOCUMENT_ROOT'])); $basePathLen = strlen($basePath); if ($basePathLen == 0) { From 5bcee0b19ba2a6a9951db6bd9f9a919ac441a4c5 Mon Sep 17 00:00:00 2001 From: Samuel Date: Sun, 26 Nov 2023 15:14:56 +0100 Subject: [PATCH 69/75] auth conception fixed --- Documentation/mvc/auth.puml | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Documentation/mvc/auth.puml b/Documentation/mvc/auth.puml index d416bc3..8a2bb1f 100644 --- a/Documentation/mvc/auth.puml +++ b/Documentation/mvc/auth.puml @@ -1,28 +1,32 @@ @startuml class AuthController { + +__construct (model : AuthModel) + displayRegister() : HttpResponse - + displayBadFields(viewName : string, fails : array) : HttpResponse - + confirmRegister(request : array) : HttpResponse + + register(request : array,session : MutableSessionHandle) : HttpResponse + displayLogin() : HttpResponse - + confirmLogin() : HttpResponse + + login(request : array , session : MutableSessionHandle) : HttpResponse } AuthController --> "- model" AuthModel class AuthModel { - + register(username : string, password : string, confirmPassword : string, email : string): array - + getAccount(email : string):array + +__construct(gateway : AccountGateway) + + register(username : string, password : string, confirmPassword : string, email : string, failures : array): Account + + generateToken() : string + login(email : string, password : string) } -AuthModel --> "- gateway" AuthGateway +AuthModel --> "- gateway" AccountGateway -class AuthGateway { +class AccountGateway { -con : Connection + +__construct(con : Connection) + + insertAccount(name : string, email : string, hash : string, token : string) : int + + getRowsFromMail(email : string): array + + getHash(email : string) : array + + exists(email : string) : bool + + getAccountFromMail(email : string ): Account + + getAccountFromToken(email : string ): Account - + mailExists(email : string) : bool - + insertAccount(username : string, hash : string, email : string) - + getUserHash(email : string):string - + getAccount (email : string): array } @enduml \ No newline at end of file From a7accbfdb5fbb4216d1421f625f0b6e66ec5051b Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sun, 26 Nov 2023 18:21:33 +0100 Subject: [PATCH 70/75] update readmes --- Documentation/README.md | 3 +++ README.md | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 Documentation/README.md diff --git a/Documentation/README.md b/Documentation/README.md new file mode 100644 index 0000000..dfc91c7 --- /dev/null +++ b/Documentation/README.md @@ -0,0 +1,3 @@ +# The wiki also exists + +Some of our explanation are contained in the [wiki](https://codefirst.iut.uca.fr/git/IQBall/Application-Web/wiki) \ No newline at end of file diff --git a/README.md b/README.md index d2edce6..9a0df84 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # IQBall - Web Application This repository hosts the IQBall application for web +## Read the docs ! +You can find some additional documentation in the [Documentation](Documentation) folder, +and in the [wiki](https://codefirst.iut.uca.fr/git/IQBall/Application-Web/wiki). + From 9ba65656df0079225201823384fd74cb297a7ac6 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sun, 26 Nov 2023 18:28:59 +0100 Subject: [PATCH 71/75] remove sample form react views --- front/views/DisplayResults.tsx | 13 ------------- front/views/SampleForm.tsx | 14 -------------- 2 files changed, 27 deletions(-) delete mode 100644 front/views/DisplayResults.tsx delete mode 100644 front/views/SampleForm.tsx diff --git a/front/views/DisplayResults.tsx b/front/views/DisplayResults.tsx deleted file mode 100644 index 7e22df3..0000000 --- a/front/views/DisplayResults.tsx +++ /dev/null @@ -1,13 +0,0 @@ -interface DisplayResultsProps { - results: readonly { name: string; description: string }[] -} - -export default function DisplayResults({ results }: DisplayResultsProps) { - const list = results.map(({ name, description }) => ( -
-

username: {name}

-

description: {description}

-
- )) - return
{list}
-} diff --git a/front/views/SampleForm.tsx b/front/views/SampleForm.tsx deleted file mode 100644 index 00309e4..0000000 --- a/front/views/SampleForm.tsx +++ /dev/null @@ -1,14 +0,0 @@ -export default function SampleForm() { - return ( -
-

Hello, this is a sample form made in react !

- - - - - - - -
- ) -} From fffb520dbf1c9454ba22ba4ef45aec782d15c544 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sun, 26 Nov 2023 14:49:01 +0100 Subject: [PATCH 72/75] fix twig assets --- ci/build_react.msh | 3 +-- public/assets | 1 + public/index.php | 3 ++- src/Api/API.php | 2 ++ src/App/Views/account_settings.twig | 2 +- src/App/Views/add_member.html.twig | 2 +- src/App/Views/delete_member.html.twig | 2 +- src/App/Views/display_login.html.twig | 2 +- src/App/Views/display_register.html.twig | 2 +- src/App/Views/display_team.html.twig | 2 +- src/App/Views/display_teams.html.twig | 4 ++-- src/App/Views/error.html.twig | 2 +- src/App/Views/home.twig | 12 ++++++------ src/App/Views/insert_team.html.twig | 2 +- src/App/Views/list_team_by_name.html.twig | 2 +- 15 files changed, 23 insertions(+), 20 deletions(-) create mode 120000 public/assets diff --git a/ci/build_react.msh b/ci/build_react.msh index 64a0cb6..32a5923 100755 --- a/ci/build_react.msh +++ b/ci/build_react.msh @@ -30,6 +30,5 @@ echo "];" >> views-mappings.php chmod +r views-mappings.php -// moshell does not supports file patterns -bash <<< "mv dist/* public/* front/assets/ front/style/ /outputs/public/" +mv dist/* front/assets/ front/style/ public/* /outputs/public/ mv views-mappings.php /outputs/ diff --git a/public/assets b/public/assets new file mode 120000 index 0000000..7b299d9 --- /dev/null +++ b/public/assets @@ -0,0 +1 @@ +../front/assets \ No newline at end of file diff --git a/public/index.php b/public/index.php index 8b4acf7..c31e289 100644 --- a/public/index.php +++ b/public/index.php @@ -31,6 +31,7 @@ use IQBall\Core\Model\TeamModel; use IQBall\Core\Validation\ValidationFail; use Twig\Environment; use Twig\Loader\FilesystemLoader; +use Twig\TwigFunction; function getConnection(): Connection { return new Connection(get_database()); @@ -62,7 +63,7 @@ function getTwig(): Environment { $fl = new FilesystemLoader("../src/App/Views"); $twig = new Environment($fl); - $twig->addGlobal("basePath", $basePath); + $twig->addFunction(new TwigFunction('path', fn(string $str) => "$basePath$str")); return $twig; } diff --git a/src/Api/API.php b/src/Api/API.php index 03daf91..f79e1b5 100644 --- a/src/Api/API.php +++ b/src/Api/API.php @@ -19,6 +19,8 @@ class API { if ($response instanceof JsonHttpResponse) { header('Content-type: application/json'); echo $response->getJson(); + } else if (get_class($response) != HttpResponse::class) { + throw new Exception("API returned unknown Http Response"); } } diff --git a/src/App/Views/account_settings.twig b/src/App/Views/account_settings.twig index 2bdd853..04d7437 100644 --- a/src/App/Views/account_settings.twig +++ b/src/App/Views/account_settings.twig @@ -17,7 +17,7 @@ - +

Paramètres

\ No newline at end of file diff --git a/src/App/Views/add_member.html.twig b/src/App/Views/add_member.html.twig index e083a63..c6bae0e 100644 --- a/src/App/Views/add_member.html.twig +++ b/src/App/Views/add_member.html.twig @@ -73,7 +73,7 @@

Ajouter un membre à votre équipe

-
+
diff --git a/src/App/Views/delete_member.html.twig b/src/App/Views/delete_member.html.twig index 5b161e3..3fa5ccd 100644 --- a/src/App/Views/delete_member.html.twig +++ b/src/App/Views/delete_member.html.twig @@ -56,7 +56,7 @@

Supprimez un membre de votre équipe

- +
diff --git a/src/App/Views/display_login.html.twig b/src/App/Views/display_login.html.twig index 48842c3..cdc11a5 100644 --- a/src/App/Views/display_login.html.twig +++ b/src/App/Views/display_login.html.twig @@ -72,7 +72,7 @@

Se connecter

- +
{% for name in fails %} diff --git a/src/App/Views/display_register.html.twig b/src/App/Views/display_register.html.twig index d83ec9e..8649de8 100644 --- a/src/App/Views/display_register.html.twig +++ b/src/App/Views/display_register.html.twig @@ -74,7 +74,7 @@

S'enregistrer

- +
{% for name in fails %} diff --git a/src/App/Views/display_team.html.twig b/src/App/Views/display_team.html.twig index 06d64fb..7f23b8b 100644 --- a/src/App/Views/display_team.html.twig +++ b/src/App/Views/display_team.html.twig @@ -57,7 +57,7 @@
-

IQBall

+

IQBall

diff --git a/src/App/Views/display_teams.html.twig b/src/App/Views/display_teams.html.twig index be9353d..1e1420a 100644 --- a/src/App/Views/display_teams.html.twig +++ b/src/App/Views/display_teams.html.twig @@ -10,7 +10,7 @@

Aucune équipe n'a été trouvée

Chercher une équipe

- +
@@ -22,7 +22,7 @@
{% else %} {% for t in teams %} -
+

Nom de l'équipe : {{ t.name }}

logo de l'équipe
diff --git a/src/App/Views/error.html.twig b/src/App/Views/error.html.twig index 7be397a..bf90319 100644 --- a/src/App/Views/error.html.twig +++ b/src/App/Views/error.html.twig @@ -51,7 +51,7 @@ {% endfor %} - + \ No newline at end of file diff --git a/src/App/Views/home.twig b/src/App/Views/home.twig index 5515ac9..5d9e8ae 100644 --- a/src/App/Views/home.twig +++ b/src/App/Views/home.twig @@ -52,9 +52,9 @@

IQ Ball

-
+
Account logo

Mon profil @@ -64,7 +64,7 @@

Mes équipes

- + {% if recentTeam != null %} {% for team in recentTeam %} @@ -78,13 +78,13 @@

Mes strategies

- + {% if recentTactic != null %} {% for tactic in recentTactic %} -
+

{{ tactic.id }} - {{ tactic.name }} - {{ tactic.creation_date }}

-
{% endfor %} diff --git a/src/App/Views/insert_team.html.twig b/src/App/Views/insert_team.html.twig index 8174fe6..65cd096 100644 --- a/src/App/Views/insert_team.html.twig +++ b/src/App/Views/insert_team.html.twig @@ -61,7 +61,7 @@

Créer une équipe

- +
diff --git a/src/App/Views/list_team_by_name.html.twig b/src/App/Views/list_team_by_name.html.twig index 858f4c6..eca5e19 100644 --- a/src/App/Views/list_team_by_name.html.twig +++ b/src/App/Views/list_team_by_name.html.twig @@ -59,7 +59,7 @@

Chercher une équipe

- +
From cb24dd53a9fcaba4afbc30bbb21e7dee475ef495 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Tue, 28 Nov 2023 16:52:30 +0100 Subject: [PATCH 73/75] Apply suggestions --- front/Fetcher.ts | 2 +- front/views/Editor.tsx | 193 ++++++++++++--------- sql/setup-tables.sql | 2 +- src/Api/Controller/APITacticController.php | 5 +- src/Core/Connection.php | 10 +- src/Core/Gateway/TacticInfoGateway.php | 38 ++-- src/Core/Model/TacticModel.php | 11 +- src/Core/Validation/ValidationFail.php | 4 + 8 files changed, 154 insertions(+), 111 deletions(-) diff --git a/front/Fetcher.ts b/front/Fetcher.ts index 7c81fdd..4c483e9 100644 --- a/front/Fetcher.ts +++ b/front/Fetcher.ts @@ -2,7 +2,7 @@ import { API } from "./Constants" export function fetchAPI( url: string, - payload: object, + payload: unknown, method = "POST", ): Promise { return fetch(`${API}/${url}`, { diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index df566ff..17da16d 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -1,4 +1,11 @@ -import React, { CSSProperties, useEffect, useRef, useState } from "react" +import { + CSSProperties, + Dispatch, + SetStateAction, + useCallback, + useRef, + useState, +} from "react" import "../style/editor.css" import TitleInput from "../components/TitleInput" import { BasketCourt } from "../components/editor/BasketCourt" @@ -11,7 +18,10 @@ import { Tactic, TacticContent } from "../tactic/Tactic" import { fetchAPI } from "../Fetcher" import { Team } from "../tactic/Team" import { calculateRatio } from "../Utils" -import SavingState, { SaveStates } from "../components/editor/SavingState" +import SavingState, { + SaveState, + SaveStates, +} from "../components/editor/SavingState" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -54,57 +64,27 @@ export default function Editor({ } function EditorView({ - tactic: { name, content }, + tactic: { name, content: initialContent }, onContentChange, onNameChange, }: EditorViewProps) { const [style, setStyle] = useState({}) - const [saveState, setSaveState] = useState(SaveStates.Ok) - - const positions = ["1", "2", "3", "4", "5"] + const [content, setContent, saveState] = useContentState( + initialContent, + (content) => + onContentChange(content).then((success) => + success ? SaveStates.Ok : SaveStates.Err, + ), + ) const [allies, setAllies] = useState( - positions - .filter( - (role) => - content.players.findIndex( - (p) => p.team == Team.Allies && p.role == role, - ) == -1, - ) - .map((key) => ({ team: Team.Allies, key })), + getRackPlayers(Team.Allies, content.players), ) const [opponents, setOpponents] = useState( - positions - .filter( - (role) => - content.players.findIndex( - (p) => p.team == Team.Opponents && p.role == role, - ) == -1, - ) - .map((key) => ({ team: Team.Opponents, key })), + getRackPlayers(Team.Opponents, content.players), ) - const [players, setPlayers] = useState(content.players) const courtDivContentRef = useRef(null) - // The didMount ref is used to store a boolean flag in order to avoid calling 'onChange' when the editor is first rendered. - const didMount = useRef(false) - useEffect(() => { - if (!didMount.current) { - didMount.current = true - return - } - setSaveState(SaveStates.Saving) - onContentChange({ players }) - .then((success) => { - if (success) { - setSaveState(SaveStates.Ok) - } else { - setSaveState(SaveStates.Err) - } - }) - .catch(() => setSaveState(SaveStates.Err)) - }, [players]) - const canDetach = (ref: HTMLDivElement) => { const refBounds = ref.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect() @@ -124,16 +104,18 @@ function EditorView({ const { x, y } = calculateRatio(refBounds, courtBounds) - setPlayers((players) => { - return [ - ...players, - { - team: element.team, - role: element.key, - rightRatio: x, - bottomRatio: y, - }, - ] + setContent((content) => { + return { + players: [ + ...content.players, + { + team: element.team, + role: element.key, + rightRatio: x, + bottomRatio: y, + }, + ], + } }) } @@ -183,47 +165,40 @@ function EditorView({
{ - setPlayers((players) => { - const idx = players.findIndex( - (p) => - p.team === player.team && - p.role === player.role, - ) - return players.toSpliced(idx, 1, player) - }) + setContent((content) => ({ + players: toSplicedPlayers( + content.players, + player, + true, + ), + })) }} onPlayerRemove={(player) => { - setPlayers((players) => { - const idx = players.findIndex( - (p) => - p.team === player.team && - p.role === player.role, - ) - return players.toSpliced(idx, 1) - }) + setContent((content) => ({ + players: toSplicedPlayers( + content.players, + player, + false, + ), + })) + let setter switch (player.team) { case Team.Opponents: - setOpponents((opponents) => [ - ...opponents, - { - team: player.team, - pos: player.role, - key: player.role, - }, - ]) + setter = setOpponents break case Team.Allies: - setAllies((allies) => [ - ...allies, - { - team: player.team, - pos: player.role, - key: player.role, - }, - ]) + setter = setAllies } + setter((players) => [ + ...players, + { + team: player.team, + pos: player.role, + key: player.role, + }, + ]) }} />
@@ -232,3 +207,51 @@ function EditorView({
) } + +function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] { + return ["1", "2", "3", "4", "5"] + .filter( + (role) => + players.findIndex((p) => p.team == team && p.role == role) == + -1, + ) + .map((key) => ({ team, key })) +} + +function useContentState( + initialContent: S, + saveStateCallback: (s: S) => Promise, +): [S, Dispatch>, SaveState] { + const [content, setContent] = useState(initialContent) + const [savingState, setSavingState] = useState(SaveStates.Ok) + + const setContentSynced = useCallback((newState: SetStateAction) => { + setContent((content) => { + const state = + typeof newState === "function" + ? (newState as (state: S) => S)(content) + : newState + if (state !== content) { + setSavingState(SaveStates.Saving) + saveStateCallback(state) + .then(setSavingState) + .catch(() => setSavingState(SaveStates.Err)) + } + return state + }) + }, []) + + return [content, setContentSynced, savingState] +} + +function toSplicedPlayers( + players: Player[], + player: Player, + replace: boolean, +): Player[] { + const idx = players.findIndex( + (p) => p.team === player.team && p.role === player.role, + ) + + return players.toSpliced(idx, 1, ...(replace ? [player] : [])) +} diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 5a2eecf..eb74877 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -44,7 +44,7 @@ CREATE TABLE Member ( id_team integer, id_user integer, - role char(1) CHECK (role IN ('Coach', 'Player')), + role text CHECK (role IN ('Coach', 'Player')), FOREIGN KEY (id_team) REFERENCES Team (id), FOREIGN KEY (id_user) REFERENCES User (id) ); diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 606e49b..51ca531 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -9,6 +9,7 @@ use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Model\TacticModel; +use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\Validators; /** @@ -55,7 +56,9 @@ class APITacticController { return Control::runChecked([ "content" => [], ], function (HttpRequest $req) use ($id) { - $this->model->updateContent($id, json_encode($req["content"])); + if ($fail = $this->model->updateContent($id, json_encode($req["content"]))) { + return new JsonHttpResponse([$fail]); + } return HttpResponse::fromCode(HttpCodes::OK); }); } diff --git a/src/Core/Connection.php b/src/Core/Connection.php index 019b515..1dd559d 100644 --- a/src/Core/Connection.php +++ b/src/Core/Connection.php @@ -25,7 +25,7 @@ class Connection { * @return void */ public function exec(string $query, array $args) { - $stmnt = $this->prepare($query, $args); + $stmnt = $this->prep($query, $args); $stmnt->execute(); } @@ -36,7 +36,7 @@ class Connection { * @return array[] the returned rows of the request */ public function fetch(string $query, array $args): array { - $stmnt = $this->prepare($query, $args); + $stmnt = $this->prep($query, $args); $stmnt->execute(); return $stmnt->fetchAll(PDO::FETCH_ASSOC); } @@ -46,7 +46,7 @@ class Connection { * @param array> $args * @return \PDOStatement */ - private function prepare(string $query, array $args): \PDOStatement { + private function prep(string $query, array $args): \PDOStatement { $stmnt = $this->pdo->prepare($query); foreach ($args as $name => $value) { $stmnt->bindValue($name, $value[0], $value[1]); @@ -54,4 +54,8 @@ class Connection { return $stmnt; } + public function prepare(string $query): \PDOStatement { + return $this->pdo->prepare($query); + } + } diff --git a/src/Core/Gateway/TacticInfoGateway.php b/src/Core/Gateway/TacticInfoGateway.php index 3e0a0d0..6b66f2c 100644 --- a/src/Core/Gateway/TacticInfoGateway.php +++ b/src/Core/Gateway/TacticInfoGateway.php @@ -74,26 +74,30 @@ class TacticInfoGateway { * update name of given tactic identifier * @param int $id * @param string $name - * @return void + * @return bool */ - public function updateName(int $id, string $name): void { - $this->con->exec( - "UPDATE Tactic SET name = :name WHERE id = :id", - [ - ":name" => [$name, PDO::PARAM_STR], - ":id" => [$id, PDO::PARAM_INT], - ] - ); + public function updateName(int $id, string $name): bool { + $stmnt = $this->con->prepare("UPDATE Tactic SET name = :name WHERE id = :id"); + $stmnt->execute([ + ":name" => $name, + ":id" => $id, + ]); + return $stmnt->rowCount() == 1; } - public function updateContent(int $id, string $json): void { - $this->con->exec( - "UPDATE Tactic SET content = :content WHERE id = :id", - [ - ":content" => [$json, PDO::PARAM_STR], - ":id" => [$id, PDO::PARAM_INT], - ] - ); + /*** + * Updates a given tactics content + * @param int $id + * @param string $json + * @return bool + */ + public function updateContent(int $id, string $json): bool { + $stmnt = $this->con->prepare("UPDATE Tactic SET content = :content WHERE id = :id"); + $stmnt->execute([ + ":content" => $json, + ":id" => $id, + ]); + return $stmnt->rowCount() == 1; } } diff --git a/src/Core/Model/TacticModel.php b/src/Core/Model/TacticModel.php index 0560568..136b27d 100644 --- a/src/Core/Model/TacticModel.php +++ b/src/Core/Model/TacticModel.php @@ -76,12 +76,17 @@ class TacticModel { return [ValidationFail::unauthorized()]; } - $this->tactics->updateName($id, $name); + if (!$this->tactics->updateName($id, $name)) { + return [ValidationFail::error("Could not update name")]; + } return []; } - public function updateContent(int $id, string $json): void { - $this->tactics->updateContent($id, $json); + public function updateContent(int $id, string $json): ?ValidationFail { + if (!$this->tactics->updateContent($id, $json)) { + return ValidationFail::error("Could not update content"); + } + return null; } } diff --git a/src/Core/Validation/ValidationFail.php b/src/Core/Validation/ValidationFail.php index 9a714e5..9a74a03 100644 --- a/src/Core/Validation/ValidationFail.php +++ b/src/Core/Validation/ValidationFail.php @@ -49,4 +49,8 @@ class ValidationFail implements JsonSerializable { return new ValidationFail("Unauthorized", $message); } + public static function error(string $message): ValidationFail { + return new ValidationFail("Error", $message); + } + } From 6ebf3737af246a69f3ff687812bb0e4308788df6 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Tue, 28 Nov 2023 20:04:01 +0100 Subject: [PATCH 74/75] apply suggestions --- front/components/editor/CourtPlayer.tsx | 7 ++----- front/views/Editor.tsx | 4 ++-- src/Api/Controller/APITacticController.php | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index 22e1bbe..12083d3 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -24,8 +24,8 @@ export default function CourtPlayer({ }: PlayerProps) { const pieceRef = useRef(null) - const [x, setX] = useState(player.rightRatio) - const [y, setY] = useState(player.bottomRatio) + const x = player.rightRatio + const y = player.bottomRatio return ( { onNameChange(new_name).then((success) => { - success ? setStyle({}) : setStyle(ERROR_STYLE) + setStyle(success ? {} : ERROR_STYLE) }) }} /> @@ -239,7 +239,7 @@ function useContentState( } return state }) - }, []) + }, [saveStateCallback]) return [content, setContentSynced, savingState] } diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 51ca531..79e766c 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -57,7 +57,7 @@ class APITacticController { "content" => [], ], function (HttpRequest $req) use ($id) { if ($fail = $this->model->updateContent($id, json_encode($req["content"]))) { - return new JsonHttpResponse([$fail]); + return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST); } return HttpResponse::fromCode(HttpCodes::OK); }); From 31c3525b1a5743c40c16fa4119abbec8df6f5e08 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Tue, 28 Nov 2023 20:57:37 +0100 Subject: [PATCH 75/75] persist in local storage if not connected --- front/views/Editor.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 13f7684..04362b5 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -27,6 +27,9 @@ const ERROR_STYLE: CSSProperties = { borderColor: "red", } +const GUEST_MODE_CONTENT_STORAGE_KEY = "guest_mode_content" +const GUEST_MODE_TITLE_STORAGE_KEY = "guest_mode_title" + export interface EditorViewProps { tactic: Tactic onContentChange: (tactic: TacticContent) => Promise @@ -52,11 +55,22 @@ export default function Editor({ }) { const isInGuestMode = id == -1 + const storage_content = localStorage.getItem(GUEST_MODE_CONTENT_STORAGE_KEY) + const editorContent = + isInGuestMode && storage_content != null ? storage_content : content + + const storage_name = localStorage.getItem(GUEST_MODE_TITLE_STORAGE_KEY) + const editorName = isInGuestMode && storage_name != null ? storage_name : name + return ( { if (isInGuestMode) { + localStorage.setItem( + GUEST_MODE_CONTENT_STORAGE_KEY, + JSON.stringify(content), + ) return SaveStates.Guest } return fetchAPI(`tactic/${id}/save`, { content }).then((r) => @@ -65,6 +79,7 @@ export default function Editor({ }} onNameChange={async (name: string) => { if (isInGuestMode) { + localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name) return true //simulate that the name has been changed } return fetchAPI(`tactic/${id}/edit/name`, { name }).then(