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/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 b41135d..2f8e22b 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 @@ -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 d95343c..c0629d4 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -1,42 +1,36 @@ @startuml -class Account { - - email: String - - phoneNumber: String +class TacticInfo { - id: int - - + setMailAddress(String) - + getMailAddress(): String - + getPhoneNumber(): String - + setPhoneNumber(String) - + getUser(): AccountUser + - name: string + - creationDate: string + - ownerId: string + + getId(): int + + getOwnerId(): int + + getCreationTimestamp(): int + + getName(): string } -Account --> "- user" AccountUser -Account --> "- teams *" Team - -interface User { - + getName(): String - + getProfilePicture(): Url - + getAge(): int -} - -class AccountUser { - - name: String - - profilePicture: Url - - age: int +class Account { + - email: string + - token: string + - name: string + - id: int - + 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 } @@ -47,19 +41,27 @@ enum MemberRole { COACH } -class Team { - - name: String - - picture: Url - + getName(): String - + getPicture(): Url +class TeamInfo { + - creationDate: int + - name: string + - picture: string + + + getName(): string + + getPicture(): string + getMainColor(): Color + getSecondColor(): Color - + listMembers(): array } -Team --> "- mainColor" Color -Team --> "- secondaryColor" Color +TeamInfo --> "- mainColor" Color +TeamInfo --> "- secondaryColor" Color + +class Team { + getInfo(): TeamInfo + listMembers(): Member[] +} + +Team --> "- info" TeamInfo Team --> "- members *" Member class Color { @@ -68,30 +70,4 @@ 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/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 87% rename from Documentation/team.puml rename to Documentation/mvc/team.puml index a291b1d..ad5e201 100644 --- a/Documentation/team.puml +++ b/Documentation/mvc/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 @@ -57,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/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/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/front/assets/icon/account.png b/front/assets/icon/account.png new file mode 100644 index 0000000..6ed3299 Binary files /dev/null and b/front/assets/icon/account.png differ diff --git a/front/assets/icon/account.svg b/front/assets/icon/account.svg new file mode 100644 index 0000000..ce59194 --- /dev/null +++ b/front/assets/icon/account.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon index bc6c041..346baaa 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,11 +3,10 @@ parameters: level: 6 paths: - src - - public scanFiles: - config.php - sql/database.php - profiles/dev-config-profile.php - profiles/prod-config-profile.php excludePaths: - - src/react-display-file.php + - src/App/react-display-file.php diff --git a/public/api/index.php b/public/api/index.php index 3ed5caa..076dd11 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -5,38 +5,33 @@ require "../../vendor/autoload.php"; require "../../sql/database.php"; require "../utils.php"; -use App\Connexion; -use App\Controller\Api\APITacticController; -use App\Gateway\TacticInfoGateway; -use App\Http\JsonHttpResponse; -use App\Http\ViewHttpResponse; -use App\Model\TacticModel; - -$con = new Connexion(get_database()); - -$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()); - -$match = $router->match(); +use IQBall\Api\API; +use IQBall\Api\Controller\APIAuthController; +use IQBall\Api\Controller\APITacticController; +use IQBall\Core\Action; +use IQBall\Core\Connection; +use IQBall\Core\Data\Account; +use IQBall\Core\Gateway\AccountGateway; +use IQBall\Core\Gateway\TacticInfoGateway; +use IQBall\Core\Model\AuthModel; +use IQBall\Core\Model\TacticModel; + +function getTacticController(): APITacticController { + return new APITacticController(new TacticModel(new TacticInfoGateway(new Connection(get_database())))); +} -if ($match == null) { - echo "404 not found"; - header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); - exit(1); +function getAuthController(): APIAuthController { + return new APIAuthController(new AuthModel(new AccountGateway(new Connection(get_database())))); } -$response = call_user_func_array($match['target'], $match['params']); +function getRoutes(): AltoRouter { + $router = new AltoRouter(); + $router->setBasePath(get_public_path() . "/api"); -http_response_code($response->getCode()); + $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())); -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."); + return $router; } + +Api::render(API::handleMatch(getRoutes()->match())); diff --git a/public/index.php b/public/index.php index 6633860..807ec88 100644 --- a/public/index.php +++ b/public/index.php @@ -1,95 +1,109 @@ 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; +function getUserController(): UserController { + return new UserController(new TacticModel(new TacticInfoGateway(getConnection()))); +} + +function getVisualizerController(): VisualizerController { + return new VisualizerController(new TacticModel(new TacticInfoGateway(getConnection()))); +} + +function getEditorController(): EditorController { + return new EditorController(new TacticModel(new TacticInfoGateway(getConnection()))); +} + +function getTeamController(): TeamController { + $con = getConnection(); + return new TeamController(new TeamModel(new TeamGateway($con), new MemberGateway($con), new AccountGateway($con))); } -$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; +function getAuthController(): AuthController { + return new AuthController(new AuthModel(new AccountGateway(getConnection()))); +} + +function getRoutes(): AltoRouter { + global $basePath; + + $ar = new AltoRouter(); + $ar->setBasePath($basePath); + + //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()->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))); + $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", 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 + $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 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); } -} elseif ($response instanceof JsonHttpResponse) { - header('Content-type: application/json'); - echo $response->getJson(); + return App::runAction($basePath . '/login', $match['target'], $match['params'], $session); } + + +//this is a global variable +$basePath = get_public_path(); + +App::render(runMatch(getRoutes()->match(), PhpSessionHandle::init()), "../src/App/Views/"); 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 e29b230..324fb39 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -1,40 +1,44 @@ -- drop tables here -DROP TABLE IF EXISTS FormEntries; -DROP TABLE IF EXISTS AccountUser; -DROP TABLE IF EXISTS TacticInfo; +DROP TABLE IF EXISTS Account; +DROP TABLE IF EXISTS Tactic; DROP TABLE IF EXISTS Team; DROP TABLE IF EXISTS User; DROP TABLE IF EXISTS Member; - -CREATE TABLE FormEntries(name varchar, description varchar); -CREATE TABLE AccountUser( - username varchar, - hash varchar, - email varchar unique +CREATE TABLE Account +( + 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 Team( - id integer PRIMARY KEY AUTOINCREMENT, - name varchar, - picture varchar, - mainColor varchar, - secondColor varchar +CREATE TABLE Tactic +( + id integer PRIMARY KEY AUTOINCREMENT, + name varchar NOT NULL, + creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + owner integer NOT NULL, + FOREIGN KEY (owner) REFERENCES Account ); -CREATE TABLE User( - id integer PRIMARY KEY AUTOINCREMENT -); +CREATE TABLE FormEntries(name varchar, description 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) + +CREATE TABLE Team +( + id integer PRIMARY KEY AUTOINCREMENT, + name varchar, + picture varchar, + main_color varchar, + second_color varchar ); -CREATE TABLE TacticInfo( - id integer PRIMARY KEY AUTOINCREMENT, - name varchar, - creation_date timestamp DEFAULT CURRENT_TIMESTAMP + +CREATE TABLE Member( + 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/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/Controller/APIAuthController.php b/src/Api/Controller/APIAuthController.php new file mode 100644 index 0000000..332f260 --- /dev/null +++ b/src/Api/Controller/APIAuthController.php @@ -0,0 +1,44 @@ +model = $model; + } + + + /** + * 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::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, HttpCodes::UNAUTHORIZED); + } + + return new JsonHttpResponse(["authorization" => $account->getToken()]); + }, true); + } + +} diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php new file mode 100644 index 0000000..63e4811 --- /dev/null +++ b/src/Api/Controller/APITacticController.php @@ -0,0 +1,48 @@ +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()], + ], 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); + } +} diff --git a/src/App/App.php b/src/App/App.php new file mode 100644 index 0000000..7fa2595 --- /dev/null +++ b/src/App/App.php @@ -0,0 +1,93 @@ +getCode()); + + foreach ($response->getHeaders() as $header => $value) { + header("$header: $value"); + } + + if ($response instanceof ViewHttpResponse) { + self::renderView($response, $twigViewsFolder); + } elseif ($response instanceof JsonHttpResponse) { + header('Content-type: application/json'); + echo $response->getJson(); + } + } + + /** + * 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(); + + switch ($response->getViewKind()) { + case ViewHttpResponse::REACT_VIEW: + send_react_front($file, $args); + break; + case ViewHttpResponse::TWIG_VIEW: + try { + $fl = new FilesystemLoader($twigViewsFolder); + $twig = new Environment($fl); + $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; + } + } + + /** + * 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 + */ + 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($authRoute); + } + } + + return $action->run($params, $session); + } + +} diff --git a/src/Controller/Control.php b/src/App/Control.php similarity index 90% rename from src/Controller/Control.php rename to src/App/Control.php index 2b428d9..3dea9b1 100644 --- a/src/Controller/Control.php +++ b/src/App/Control.php @@ -1,14 +1,14 @@ model = $model; + } + + public function displayRegister(): HttpResponse { + return ViewHttpResponse::twig("display_register.html.twig", []); + } + + /** + * registers given account + * @param mixed[] $request + * @param MutableSessionHandle $session + * @return 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::email(), Validators::lenBetween(5, 256)], + ]); + if (!empty($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 ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]); + } + + $session->setAccount($account); + + $target_url = $session->getInitialTarget(); + return HttpResponse::redirect($target_url ?? "/home"); + } + + + public function displayLogin(): HttpResponse { + return ViewHttpResponse::twig("display_login.html.twig", []); + } + + /** + * logins given account credentials + * @param mixed[] $request + * @param MutableSessionHandle $session + * @return HttpResponse + */ + public function login(array $request, MutableSessionHandle $session): HttpResponse { + $fails = []; + $request = HttpRequest::from($request, $fails, [ + "password" => [Validators::lenBetween(6, 256)], + "email" => [Validators::email(), Validators::lenBetween(5, 256)], + ]); + if (!empty($fails)) { + return ViewHttpResponse::twig("display_login.html.twig", ['fails' => $fails]); + } + + $account = $this->model->login($request['email'], $request['password'], $fails); + if (!empty($fails)) { + return ViewHttpResponse::twig("display_login.html.twig", ['fails' => $fails]); + } + + $session->setAccount($account); + + $target_url = $session->getInitialTarget(); + $session->setInitialTarget(null); + return HttpResponse::redirect($target_url ?? "/home"); + } + +} diff --git a/src/App/Controller/EditorController.php b/src/App/Controller/EditorController.php new file mode 100644 index 0000000..6724ced --- /dev/null +++ b/src/App/Controller/EditorController.php @@ -0,0 +1,57 @@ +model = $model; + } + + /** + * @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()]); + } + + /** + * 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->openEditorFor($tactic); + } + + /** + * returns an editor view for a given tactic + * @param int $id the targeted tactic identifier + * @param SessionHandle $session + * @return ViewHttpResponse + */ + public function openEditor(int $id, SessionHandle $session): ViewHttpResponse { + $tactic = $this->model->get($id); + + $failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId()); + + if ($failure != null) { + return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); + } + + return $this->openEditorFor($tactic); + } + + +} diff --git a/src/App/Controller/TeamController.php b/src/App/Controller/TeamController.php new file mode 100644 index 0000000..deaf61b --- /dev/null +++ b/src/App/Controller/TeamController.php @@ -0,0 +1,155 @@ +model = $model; + } + + /** + * @param SessionHandle $session + * @return ViewHttpResponse the team creation panel + */ + public function displayCreateTeam(SessionHandle $session): ViewHttpResponse { + return ViewHttpResponse::twig("insert_team.html.twig", []); + } + + + /** + * @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", []); + } + + + /** + * @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 + */ + public function submitTeam(array $request, SessionHandle $session): HttpResponse { + $failures = []; + $request = HttpRequest::from($request, $failures, [ + "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], + "main_color" => [Validators::hexColor()], + "second_color" => [Validators::hexColor()], + "picture" => [Validators::isURL()], + ]); + if (!empty($failures)) { + $badFields = []; + foreach ($failures as $e) { + if ($e instanceof FieldValidationFail) { + $badFields[] = $e->getFieldName(); + } + } + return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]); + } + $teamId = $this->model->createTeam($request['name'], $request['picture'], $request['main_color'], $request['second_color']); + return $this->displayTeam($teamId, $session); + } + + /** + * @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", []); + } + + /** + * 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 { + $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]); + } + + $teams = $this->model->listByName($request['name']); + + if (empty($teams)) { + return ViewHttpResponse::twig('display_teams.html.twig', []); + } + + return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $teams]); + } + + /** + * @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 + */ + public function addMember(array $request, SessionHandle $session): HttpResponse { + $errors = []; + + $request = HttpRequest::from($request, $errors, [ + "team" => [Validators::isInteger()], + "email" => [Validators::email(), Validators::lenBetween(5, 256)], + ]); + + $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 + */ + public function deleteMember(array $request, SessionHandle $session): HttpResponse { + $errors = []; + + $request = HttpRequest::from($request, $errors, [ + "team" => [Validators::isInteger()], + "email" => [Validators::email(), Validators::lenBetween(5, 256)], + ]); + + 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 new file mode 100644 index 0000000..b7cef0b --- /dev/null +++ b/src/App/Controller/UserController.php @@ -0,0 +1,37 @@ +tactics = $tactics; + } + + /** + * @param SessionHandle $session + * @return ViewHttpResponse the home page view + */ + 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 ViewHttpResponse account settings page + */ + 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 new file mode 100644 index 0000000..48e1168 --- /dev/null +++ b/src/App/Controller/VisualizerController.php @@ -0,0 +1,39 @@ +tacticModel = $tacticModel; + } + + /** + * 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()); + + 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/Http/ViewHttpResponse.php b/src/App/ViewHttpResponse.php similarity index 93% rename from src/Http/ViewHttpResponse.php rename to src/App/ViewHttpResponse.php index 2e517d7..dfbd1da 100644 --- a/src/Http/ViewHttpResponse.php +++ b/src/App/ViewHttpResponse.php @@ -1,6 +1,9 @@ $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/App/Views/account_settings.twig b/src/App/Views/account_settings.twig new file mode 100644 index 0000000..3d8ead3 --- /dev/null +++ b/src/App/Views/account_settings.twig @@ -0,0 +1,24 @@ + + + + + + + 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 new file mode 100644 index 0000000..6c5a3e3 --- /dev/null +++ b/src/App/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/App/Views/delete_member.html.twig b/src/App/Views/delete_member.html.twig new file mode 100644 index 0000000..b7d0d3b --- /dev/null +++ b/src/App/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_auth_confirm.html.twig b/src/App/Views/display_auth_confirm.html.twig similarity index 100% rename from src/Views/display_auth_confirm.html.twig rename to src/App/Views/display_auth_confirm.html.twig diff --git a/src/Views/display_login.html.twig b/src/App/Views/display_login.html.twig similarity index 83% rename from src/Views/display_login.html.twig rename to src/App/Views/display_login.html.twig index 33b2385..ca6890d 100644 --- a/src/Views/display_login.html.twig +++ b/src/App/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/App/Views/display_register.html.twig similarity index 85% rename from src/Views/display_register.html.twig rename to src/App/Views/display_register.html.twig index 40199a0..2b24e23 100644 --- a/src/Views/display_register.html.twig +++ b/src/App/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 %} + diff --git a/src/Views/display_results.html.twig b/src/App/Views/display_results.html.twig similarity index 100% rename from src/Views/display_results.html.twig rename to src/App/Views/display_results.html.twig diff --git a/src/Views/display_team.html.twig b/src/App/Views/display_team.html.twig similarity index 63% rename from src/Views/display_team.html.twig rename to src/App/Views/display_team.html.twig index ada8566..6f0aec1 100644 --- a/src/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{ @@ -54,6 +50,7 @@ height: 80px; width: 80px; } + @@ -65,15 +62,21 @@
-

{{ team.name }}

- +

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

+
-

Couleur principale :

-

Couleur secondaire :

+

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/App/Views/display_teams.html.twig similarity index 93% rename from src/Views/display_teams.html.twig rename to src/App/Views/display_teams.html.twig index c0ac185..e4da64e 100644 --- a/src/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

- +
diff --git a/src/Views/error.html.twig b/src/App/Views/error.html.twig similarity index 91% rename from src/Views/error.html.twig rename to src/App/Views/error.html.twig index 1d1db7d..e30c2fa 100644 --- a/src/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 new file mode 100644 index 0000000..acf6f6b --- /dev/null +++ b/src/App/Views/home.twig @@ -0,0 +1,94 @@ + + + + + + + Page d'accueil + + + + + +
+

IQ Ball

+
+ Account logo +

Mon profil

+

+
+ +

Mes équipes

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

{{team.name}}

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

Aucune équipe créé !

+ {% endif %} + +

Mes strategies

+ + + + {% 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/Views/insert_team.html.twig b/src/App/Views/insert_team.html.twig similarity index 86% rename from src/Views/insert_team.html.twig rename to src/App/Views/insert_team.html.twig index 0cc85dd..0eafb55 100644 --- a/src/Views/insert_team.html.twig +++ b/src/App/Views/insert_team.html.twig @@ -70,10 +70,10 @@ - - - - + + + +
diff --git a/src/Views/list_team_by_name.html.twig b/src/App/Views/list_team_by_name.html.twig similarity index 96% rename from src/Views/list_team_by_name.html.twig rename to src/App/Views/list_team_by_name.html.twig index 1d9ddf3..09768e6 100644 --- a/src/Views/list_team_by_name.html.twig +++ b/src/App/Views/list_team_by_name.html.twig @@ -62,7 +62,7 @@

Chercher une équipe

- +
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/Api/APITacticController.php b/src/Controller/Api/APITacticController.php deleted file mode 100644 index ec0edc8..0000000 --- a/src/Controller/Api/APITacticController.php +++ /dev/null @@ -1,55 +0,0 @@ -model = $model; - } - - public function updateName(int $tactic_id): HttpResponse { - return Control::runChecked([ - "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); - }, true); - } - - public function newTactic(): HttpResponse { - return Control::runChecked([ - "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], - ], function (HttpRequest $request) { - $tactic = $this->model->makeNew($request["name"]); - $id = $tactic->getId(); - return new JsonHttpResponse(["id" => $id]); - }, true); - } - - public function getTacticInfo(int $id): 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/Controller/AuthController.php b/src/Controller/AuthController.php deleted file mode 100644 index e42c27d..0000000 --- a/src/Controller/AuthController.php +++ /dev/null @@ -1,94 +0,0 @@ -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/Controller/EditorController.php b/src/Controller/EditorController.php deleted file mode 100644 index ed270d1..0000000 --- a/src/Controller/EditorController.php +++ /dev/null @@ -1,47 +0,0 @@ -model = $model; - } - - private function openEditor(TacticInfo $tactic): HttpResponse { - return ViewHttpResponse::react("views/Editor.tsx", ["name" => $tactic->getName(), "id" => $tactic->getId()]); - } - - public function makeNew(): HttpResponse { - $tactic = $this->model->makeNewDefault(); - return $this->openEditor($tactic); - } - - /** - * returns an editor view for a given tactic - * @param int $id the targeted tactic identifier - * @return HttpResponse - */ - public function openEditorFor(int $id): HttpResponse { - $tactic = $this->model->get($id); - - if ($tactic == null) { - return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND); - } - - return $this->openEditor($tactic); - } - -} 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/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/TeamController.php b/src/Controller/TeamController.php deleted file mode 100644 index 08e22cc..0000000 --- a/src/Controller/TeamController.php +++ /dev/null @@ -1,83 +0,0 @@ -model = $model; - } - - public function displaySubmitTeam(): HttpResponse { - return ViewHttpResponse::twig("insert_team.html.twig", []); - } - - /** - * @param array $request - * @return HttpResponse - */ - public function submitTeam(array $request): HttpResponse { - $errors = []; - - $request = HttpRequest::from($request, $errors, [ - "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)) { - $badFields = []; - foreach ($errors as $e) { - if ($e instanceof FieldValidationFail) { - $badFields[] = $e->getFieldName(); - } - } - return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]); - } - return $this->getTeam($this->model->createTeam($request['name'], $request['picture'], $request['mainColor'], $request['secondColor'])); - } - - public function displayListTeamByName(): HttpResponse { - 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()], - ]); - - 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]); - } - - public function getTeam(int $id): HttpResponse { - $result = $this->model->displayTeam($id); - return ViewHttpResponse::twig('display_team.html.twig', ['team' => $result]); - } -} diff --git a/src/Controller/VisualizerController.php b/src/Controller/VisualizerController.php deleted file mode 100644 index 3c8b55e..0000000 --- a/src/Controller/VisualizerController.php +++ /dev/null @@ -1,32 +0,0 @@ -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()]); - - } -} diff --git a/src/Core/Action.php b/src/Core/Action.php new file mode 100644 index 0000000..35721c1 --- /dev/null +++ b/src/Core/Action.php @@ -0,0 +1,58 @@ +action = $action; + $this->isAuthRequired = $isAuthRequired; + } + + public function isAuthRequired(): bool { + return $this->isAuthRequired; + } + + /** + * Runs an action + * @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/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 @@ email = $email; + $this->name = $name; + $this->token = $token; + $this->id = $id; + } + + public function getId(): int { + return $this->id; + } + + public function getEmail(): string { + return $this->email; + } + + public function getToken(): string { + return $this->token; + } + + public function getName(): string { + return $this->name; + } + +} diff --git a/src/Data/Color.php b/src/Core/Data/Color.php similarity index 83% rename from src/Data/Color.php rename to src/Core/Data/Color.php index b12e27a..e0cd27c 100755 --- a/src/Data/Color.php +++ b/src/Core/Data/Color.php @@ -1,6 +1,6 @@ 0xFFFFFF) { - throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF"); - } $this->hex = $value; } diff --git a/src/Data/Member.php b/src/Core/Data/Member.php similarity index 60% rename from src/Data/Member.php rename to src/Core/Data/Member.php index b415f65..d68140c 100755 --- a/src/Data/Member.php +++ b/src/Core/Data/Member.php @@ -1,16 +1,21 @@ userId = $userId; + $this->teamId = $teamId; $this->role = $role; } @@ -39,4 +45,11 @@ class Member { public function getRole(): MemberRole { return $this->role; } + + /** + * @return int + */ + public function getTeamId(): int { + return $this->teamId; + } } diff --git a/src/Data/MemberRole.php b/src/Core/Data/MemberRole.php similarity index 60% rename from src/Data/MemberRole.php rename to src/Core/Data/MemberRole.php index 559d516..41b6b71 100755 --- a/src/Data/MemberRole.php +++ b/src/Core/Data/MemberRole.php @@ -1,13 +1,13 @@ 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/Data/TacticInfo.php b/src/Core/Data/TacticInfo.php similarity index 54% rename from src/Data/TacticInfo.php rename to src/Core/Data/TacticInfo.php index eef7bb3..04f592a 100644 --- a/src/Data/TacticInfo.php +++ b/src/Core/Data/TacticInfo.php @@ -1,21 +1,24 @@ id = $id; $this->name = $name; - $this->creation_date = $creation_date; + $this->ownerId = $ownerId; + $this->creationDate = $creationDate; } public function getId(): int { @@ -26,14 +29,15 @@ class TacticInfo implements \JsonSerializable { return $this->name; } - public function getCreationTimestamp(): int { - return $this->creation_date; - } - /** - * @return array + * @return int */ - public function jsonSerialize(): array { - return get_object_vars($this); + public function getOwnerId(): int { + return $this->ownerId; } + + public function getCreationTimestamp(): int { + return $this->creationDate; + } + } diff --git a/src/Core/Data/Team.php b/src/Core/Data/Team.php new file mode 100755 index 0000000..b8e7834 --- /dev/null +++ b/src/Core/Data/Team.php @@ -0,0 +1,32 @@ +info = $info; + $this->members = $members; + } + + public function getInfo(): TeamInfo { + return $this->info; + } + + /** + * @return Member[] + */ + public function listMembers(): array { + return $this->members; + } +} diff --git a/src/Data/Team.php b/src/Core/Data/TeamInfo.php old mode 100755 new mode 100644 similarity index 62% rename from src/Data/Team.php rename to src/Core/Data/TeamInfo.php index e50ad40..7affcea --- a/src/Data/Team.php +++ b/src/Core/Data/TeamInfo.php @@ -1,8 +1,8 @@ id = $id; $this->name = $name; $this->picture = $picture; $this->mainColor = $mainColor; $this->secondColor = $secondColor; - $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; } - /** - * @return Member[] - */ - public function listMembers(): array { - return $this->members; - } } diff --git a/src/Core/Gateway/AccountGateway.php b/src/Core/Gateway/AccountGateway.php new file mode 100644 index 0000000..7740b57 --- /dev/null +++ b/src/Core/Gateway/AccountGateway.php @@ -0,0 +1,85 @@ +con = $con; + } + + + 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], + ]); + return intval($this->con->lastInsertId()); + } + + /** + * @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; + } + + /** + * @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) { + return null; + } + 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; + } + + /** + * @param string $email + * @return Account|null + */ + 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"]); + } + + /** + * @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)) { + return null; + } + + return new Account($acc["email"], $acc["username"], $acc["token"], $acc["id"]); + } + + +} 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 new file mode 100644 index 0000000..f3d2432 --- /dev/null +++ b/src/Core/Gateway/TacticInfoGateway.php @@ -0,0 +1,93 @@ +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", + [":id" => [$id, PDO::PARAM_INT]] + ); + + if (!isset($res[0])) { + return null; + } + + $row = $res[0]; + + return new TacticInfo($id, $row["name"], strtotime($row["creation_date"]), $row["owner"]); + } + + + /** + * Return the nb last tactics created + * + * @param integer $nb + * @return 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]] + ); + if (count($res) == 0) { + return null; + } + 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)", + [ + ":name" => [$name, PDO::PARAM_STR], + ":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"]); + } + + /** + * 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", + [ + ":name" => [$name, PDO::PARAM_STR], + ":id" => [$id, PDO::PARAM_INT], + ] + ); + } + +} diff --git a/src/Core/Gateway/TeamGateway.php b/src/Core/Gateway/TeamGateway.php new file mode 100644 index 0000000..d775eda --- /dev/null +++ b/src/Core/Gateway/TeamGateway.php @@ -0,0 +1,85 @@ +con = $con; + } + + /** + * @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, main_color, second_color) VALUES (:team_name , :picture, :main_color, :second_color)", + [ + ":team_name" => [$name, PDO::PARAM_STR], + ":picture" => [$picture, PDO::PARAM_STR], + ":main_color" => [$mainColor, PDO::PARAM_STR], + ":second_color" => [$secondColor, PDO::PARAM_STR], + ] + ); + return intval($this->con->lastInsertId()); + } + + + /** + * @param string $name + * @return TeamInfo[] + */ + public function listByName(string $name): array { + $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 TeamInfo + */ + 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 getTeamIdByName(string $name): ?int { + return $this->con->fetch( + "SELECT id FROM Team WHERE name = :name", + [ + ":name" => [$name, PDO::PARAM_INT], + ] + )[0]['id'] ?? null; + } + + +} diff --git a/src/Http/HttpCodes.php b/src/Core/Http/HttpCodes.php similarity index 59% rename from src/Http/HttpCodes.php rename to src/Core/Http/HttpCodes.php index f9d550c..1903f0c 100644 --- a/src/Http/HttpCodes.php +++ b/src/Core/Http/HttpCodes.php @@ -1,13 +1,18 @@ + */ + private array $headers; + private int $code; + + /** + * @param int $code + * @param array $headers + */ + 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; + } + + /** + * @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"); + } + return new HttpResponse($code, ["Location" => $url]); + } + +} diff --git a/src/Http/JsonHttpResponse.php b/src/Core/Http/JsonHttpResponse.php similarity index 88% rename from src/Http/JsonHttpResponse.php rename to src/Core/Http/JsonHttpResponse.php index bbd3d80..bb897f7 100644 --- a/src/Http/JsonHttpResponse.php +++ b/src/Core/Http/JsonHttpResponse.php @@ -1,6 +1,6 @@ payload = $payload; } diff --git a/src/Core/Model/AuthModel.php b/src/Core/Model/AuthModel.php new file mode 100644 index 0000000..a6ada53 --- /dev/null +++ b/src/Core/Model/AuthModel.php @@ -0,0 +1,80 @@ +gateway = $gateway; + } + + + /** + * @param string $username + * @param string $password + * @param string $confirmPassword + * @param string $email + * @param ValidationFail[] $failures + * @return Account|null the registered account or null if failures occurred + */ + public function register(string $username, string $password, string $confirmPassword, string $email, array &$failures): ?Account { + + if ($password != $confirmPassword) { + $failures[] = new FieldValidationFail("confirmpassword", "Le mot de passe et la confirmation ne sont pas les mêmes."); + } + + if ($this->gateway->exists($email)) { + $failures[] = new FieldValidationFail("email", "L'email existe déjà"); + } + + if (!empty($failures)) { + return null; + } + + $hash = password_hash($password, PASSWORD_DEFAULT); + + $token = $this->generateToken(); + $accountId = $this->gateway->insertAccount($username, $email, $token, $hash); + 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)); + } + + /** + * @param string $email + * @param string $password + * @param ValidationFail[] $failures + * @return Account|null the authenticated account or null if failures occurred + */ + public function login(string $email, string $password, array &$failures): ?Account { + $hash = $this->gateway->getHash($email); + if ($hash == null) { + $failures[] = new FieldValidationFail("email", "l'addresse email n'est pas connue."); + return null; + } + + if (!password_verify($password, $hash)) { + $failures[] = new FieldValidationFail("password", "Mot de passe invalide."); + return null; + } + + return $this->gateway->getAccountFromMail($email); + } + + +} diff --git a/src/Core/Model/TacticModel.php b/src/Core/Model/TacticModel.php new file mode 100644 index 0000000..64c6ca3 --- /dev/null +++ b/src/Core/Model/TacticModel.php @@ -0,0 +1,82 @@ +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->makeNew(self::TACTIC_DEFAULT_NAME, $ownerId); + } + + /** + * Tries to retrieve information about a tactic + * @param int $id tactic identifier + * @return TacticInfo|null or null if the identifier did not match a tactic + */ + public function get(int $id): ?TacticInfo { + return $this->tactics->get($id); + } + + /** + * Return the nb last tactics created + * + * @param integer $nb + * @return array> + */ + public function getLast(int $nb): ?array { + return $this->tactics->getLast($nb); + } + + /** + * Update the name of a tactic + * @param int $id the tactic identifier + * @param string $name the new name to set + * @return ValidationFail[] failures, if any + */ + 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 []; + } + +} diff --git a/src/Core/Model/TeamModel.php b/src/Core/Model/TeamModel.php new file mode 100644 index 0000000..f6af837 --- /dev/null +++ b/src/Core/Model/TeamModel.php @@ -0,0 +1,82 @@ +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 { + return $this->teams->insert($name, $picture, $mainColor, $secondColor); + } + + /** + * 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 TeamInfo[] + */ + public function listByName(string $name): array { + return $this->teams->listByName($name); + } + + /** + * @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 { + $userId = $this->users->getAccountFromMail($mail)->getId(); + $this->members->remove($teamId, $userId); + return $teamId; + } + +} diff --git a/src/Core/Session/MutableSessionHandle.php b/src/Core/Session/MutableSessionHandle.php new file mode 100644 index 0000000..1d7ae86 --- /dev/null +++ b/src/Core/Session/MutableSessionHandle.php @@ -0,0 +1,20 @@ + $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); + 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/Validation/Validator.php b/src/Core/Validation/Validator.php similarity index 95% rename from src/Validation/Validator.php rename to src/Core/Validation/Validator.php index 8227e46..8624bfe 100644 --- a/src/Validation/Validator.php +++ b/src/Core/Validation/Validator.php @@ -1,6 +1,6 @@ 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 `_`. */ @@ -41,16 +49,23 @@ 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 []; } ); } + 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 new file mode 100644 index 0000000..6c9a57b --- /dev/null +++ b/src/Core/Validator/TacticValidator.php @@ -0,0 +1,26 @@ +getOwnerId() != $ownerId) { + return ValidationFail::unauthorized("Vous ne pouvez pas accéder à cette tactique."); + } + + return null; + } +} diff --git a/src/Data/Account.php b/src/Data/Account.php deleted file mode 100755 index 2a21bf1..0000000 --- a/src/Data/Account.php +++ /dev/null @@ -1,105 +0,0 @@ -email = $email; - $this->phoneNumber = $phoneNumber; - $this->user = $user; - $this->teams = $teams; - $this->id = $id; - } - - /** - * @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; - } - - /** - * @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 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/Data/AccountUser.php b/src/Data/AccountUser.php deleted file mode 100755 index 5d3b497..0000000 --- a/src/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/Data/User.php b/src/Data/User.php deleted file mode 100755 index 6cb55c2..0000000 --- a/src/Data/User.php +++ /dev/null @@ -1,26 +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/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 deleted file mode 100644 index 3441c9a..0000000 --- a/src/Gateway/TacticInfoGateway.php +++ /dev/null @@ -1,56 +0,0 @@ -con = $con; - } - - public function get(int $id): ?TacticInfo { - $res = $this->con->fetch( - "SELECT * FROM TacticInfo WHERE id = :id", - [":id" => [$id, PDO::PARAM_INT]] - ); - - if (!isset($res[0])) { - return null; - } - - $row = $res[0]; - - return new TacticInfo($id, $row["name"], strtotime($row["creation_date"])); - } - - public function insert(string $name): TacticInfo { - $this->con->exec( - "INSERT INTO TacticInfo(name) VALUES(:name)", - [":name" => [$name, PDO::PARAM_STR]] - ); - $row = $this->con->fetch( - "SELECT id, creation_date FROM TacticInfo WHERE :id = id", - [':id' => [$this->con->lastInsertId(), PDO::PARAM_INT]] - )[0]; - return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"])); - } - - 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], - ] - ); - } - -} diff --git a/src/Gateway/TeamGateway.php b/src/Gateway/TeamGateway.php deleted file mode 100644 index c3d22dc..0000000 --- a/src/Gateway/TeamGateway.php +++ /dev/null @@ -1,79 +0,0 @@ -con = $con; - } - - 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], - ] - ); - } - - /** - * @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], - ] - ); - } - - /** - * @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], - ] - ); - } - - /** - * @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], - ] - ); - } - - /** - * @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], - ] - ); - } - -} diff --git a/src/Http/HttpResponse.php b/src/Http/HttpResponse.php deleted file mode 100644 index 5d8c3bf..0000000 --- a/src/Http/HttpResponse.php +++ /dev/null @@ -1,23 +0,0 @@ -code = $code; - } - - public function getCode(): int { - return $this->code; - } - - public static function fromCode(int $code): HttpResponse { - return new HttpResponse($code); - } - -} diff --git a/src/Model/AuthModel.php b/src/Model/AuthModel.php deleted file mode 100644 index 45b63e4..0000000 --- a/src/Model/AuthModel.php +++ /dev/null @@ -1,80 +0,0 @@ -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/Model/TacticModel.php b/src/Model/TacticModel.php deleted file mode 100644 index cabcdec..0000000 --- a/src/Model/TacticModel.php +++ /dev/null @@ -1,53 +0,0 @@ -tactics = $tactics; - } - - public function makeNew(string $name): TacticInfo { - return $this->tactics->insert($name); - } - - public function makeNewDefault(): ?TacticInfo { - return $this->tactics->insert(self::TACTIC_DEFAULT_NAME); - } - - /** - * Tries to retrieve information about a tactic - * @param int $id tactic identifier - * @return TacticInfo|null or null if the identifier did not match a tactic - */ - public function get(int $id): ?TacticInfo { - return $this->tactics->get($id); - } - - /** - * 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 - */ - public function updateName(int $id, string $name): bool { - if ($this->tactics->get($id) == null) { - return false; - } - - $this->tactics->updateName($id, $name); - return true; - } - -} diff --git a/src/Model/TeamModel.php b/src/Model/TeamModel.php deleted file mode 100644 index 65f22a7..0000000 --- a/src/Model/TeamModel.php +++ /dev/null @@ -1,54 +0,0 @@ -gateway = $gateway; - } - - 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']); - } - - /** - * @param string $name - * @return Team[] - */ - 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; - } - - public function displayTeam(int $id): Team { - $members = []; - $result = $this->gateway->getTeamById($id)[0]; - $resultMembers = $this->gateway->getMembersById($id); - foreach ($resultMembers as $row) { - if ($row['role'] == 'C') { - $role = MemberRole::coach(); - } else { - $role = MemberRole::player(); - } - $members[] = new Member($row['id'], $role); - } - return new Team(intval($result['id']), $result['name'], $result['picture'], Color::from($result['mainColor']), Color::from($result['secondColor']), $members); - } -} diff --git a/src/Views/sample_form.html.twig b/src/Views/sample_form.html.twig deleted file mode 100644 index 6f4a9b5..0000000 --- a/src/Views/sample_form.html.twig +++ /dev/null @@ -1,19 +0,0 @@ - - - - Twig view - - - -

Hello, this is a sample form made in Twig !

- - - - - - - - - - - \ No newline at end of file diff --git a/public/utils.php b/src/utils.php similarity index 100% rename from public/utils.php rename to src/utils.php