Merge branch 'salva' of codefirst.iut.uca.fr:IQBall/Application-Web into welcome-page

pull/18/head
DahmaneYanis 1 year ago
commit 167db8ef3e

2
.gitignore vendored

@ -4,7 +4,7 @@
.vite .vite
vendor vendor
.nfs*
composer.lock composer.lock
*.phar *.phar
/dist /dist

@ -2,12 +2,10 @@
object Account { object Account {
<u>id <u>id
name
age
email email
phoneNumber username
passwordHash token
profilePicture hash
} }
object Team { object Team {
@ -26,7 +24,7 @@ object TacticFolder {
object Tactic { object Tactic {
<u>id_json <u>id_json
name name
creationDate creation_date
} }
usecase have_team [ usecase have_team [
@ -63,6 +61,10 @@ usecase contains_other_folder [
to contain to contain
] ]
usecase owns [
owns
]
Account "0,n" -- have_team Account "0,n" -- have_team
have_team -- "1,n" Team have_team -- "1,n" Team
@ -73,6 +75,9 @@ shared_tactic_account -- "0,n" Tactic
Tactic "0,n" -- shared_tactic_team Tactic "0,n" -- shared_tactic_team
shared_tactic_team -- "0,n" Team shared_tactic_team -- "0,n" Team
Tactic "1,1" -- owns
owns -- Account
Team "0,n" -- shared_folder_team Team "0,n" -- shared_folder_team
shared_folder_team -- "0,n"TacticFolder shared_folder_team -- "0,n"TacticFolder

@ -81,7 +81,7 @@ AuthController --> "- model" AuthModel
class AuthModel{ class AuthModel{
+ register(username : string, password : string, confirmPassword : string, email : string): array + register(username : string, password : string, confirmPassword : string, email : string): array
+ getUserFields(email : string):array + getAccount(email : string):array
+ login(email : string, password : string) + login(email : string, password : string)
} }
AuthModel --> "- gateway" AuthGateway AuthModel --> "- gateway" AuthGateway
@ -89,9 +89,9 @@ AuthModel --> "- gateway" AuthGateway
class AuthGateway{ class AuthGateway{
-con : Connection -con : Connection
+ mailExist(email : string) : bool + mailExists(email : string) : bool
+ insertAccount(username : string, hash : string, email : string) + insertAccount(username : string, hash : string, email : string)
+ getUserHash(email : string):string + getHash(email : string):string
+ getUserFields (email : string): array + getAccount (email : string): array
} }
@enduml @enduml

@ -0,0 +1,68 @@
@startuml
class Team {
- name: String
- picture: Url
- members: array<int, MemberRole>
+ getName(): String
+ getPicture(): Url
+ getMainColor(): Color
+ getSecondColor(): Color
+ listMembers(): array<Member>
}
Team --> "- mainColor" Color
Team --> "- secondColor" Color
class Color {
- value: string
- __construct(value : string)
+ getValue(): string
+ from(value: string): Color
+ tryFrom(value : string) : ?Color
}
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{
---
+ __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
TeamModel ..> Team
TeamModel ..> Color
class TeamController{
- twig : Environement
--
+ __construct( model : TeamModel, twig : Environement)
+ displaySubmitTeam() : HttpResponse
+ submitTeam(request : array) : HttpResponse
+ displayListTeamByName(): HttpResponse
+ listTeamByName(request : array) : HttpResponse
+ displayTeam(id : int): HttpResponse
}
TeamController *--"- model" TeamModel
class Connexion{
- pdo : PDO
--
+ __constructor(pdo : PDO)
+ exec(query : string, args : array)
+ fetch(query string, args array): array
}
@enduml

@ -9,5 +9,7 @@ parameters:
- sql/database.php - sql/database.php
- profiles/dev-config-profile.php - profiles/dev-config-profile.php
- profiles/prod-config-profile.php - profiles/prod-config-profile.php
- public/api/index.php
excludePaths: excludePaths:
- src/react-display-file.php - src/react-display-file.php
- public/api/index.php

@ -6,32 +6,137 @@ require "../../sql/database.php";
require "../utils.php"; require "../utils.php";
use App\Connexion; use App\Connexion;
use App\Controller\Api\APIAuthController;
use App\Controller\Api\APITacticController; use App\Controller\Api\APITacticController;
use App\Data\Account;
use App\Gateway\AccountGateway;
use App\Gateway\TacticInfoGateway; use App\Gateway\TacticInfoGateway;
use App\Http\HttpResponse;
use App\Http\JsonHttpResponse; use App\Http\JsonHttpResponse;
use App\Http\ViewHttpResponse; use App\Http\ViewHttpResponse;
use App\Model\AuthModel;
use App\Model\TacticModel; use App\Model\TacticModel;
use App\Session\PhpSessionHandle;
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()))));
}
/**
* A Front controller action
*/
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 = PhpSessionHandle::init();
return $session->getAccount();
}
$token = $headers['Authorization'];
$gateway = new AccountGateway(new Connexion(get_database()));
return $gateway->getAccountFromToken($token);
}
$router = new AltoRouter(); $router = new AltoRouter();
$router->setBasePath(get_public_path() . "/api"); $router->setBasePath(get_public_path() . "/api");
$tacticEndpoint = new APITacticController(new TacticModel(new TacticInfoGateway($con))); $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc)));
$router->map("POST", "/tactic/[i:id]/edit/name", fn(int $id) => $tacticEndpoint->updateName($id)); $router->map("GET", "/tactic/[i:id]", Action::auth(fn(int $id, Account $acc) => getTacticController()->getTacticInfo($id, $acc)));
$router->map("GET", "/tactic/[i:id]", fn(int $id) => $tacticEndpoint->getTacticInfo($id)); $router->map("POST", "/tactic/new", Action::auth(fn(Account $acc) => getTacticController()->newTactic($acc)));
$router->map("POST", "/tactic/new", fn() => $tacticEndpoint->newTactic()); $router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize()));
$match = $router->match(); $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()); http_response_code($response->getCode());
if ($response instanceof JsonHttpResponse) { if ($response instanceof JsonHttpResponse) {

@ -1,5 +1,6 @@
<?php <?php
require "../vendor/autoload.php"; require "../vendor/autoload.php";
require "../config.php"; require "../config.php";
require "../sql/database.php"; require "../sql/database.php";
@ -7,7 +8,10 @@ require "utils.php";
require "../src/react-display.php"; require "../src/react-display.php";
use App\Controller\FrontController; use App\Controller\FrontController;
use App\Session\PhpSessionHandle;
$basePath = get_public_path(); $basePath = get_public_path();
$frontController = new FrontController($basePath); $frontController = new FrontController($basePath);
$frontController->run();
$frontController->run(PhpSessionHandle::init());

@ -1,17 +1,52 @@
-- drop tables here -- drop tables here
DROP TABLE IF EXISTS FormEntries; DROP TABLE IF EXISTS Account;
DROP TABLE IF EXISTS AccountUser; DROP TABLE IF EXISTS Tactic;
DROP TABLE IF EXISTS TacticInfo; DROP TABLE IF EXISTS Team;
DROP TABLE IF EXISTS User;
DROP TABLE IF EXISTS Member;
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 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 Team
(
id integer PRIMARY KEY AUTOINCREMENT,
name varchar,
picture varchar,
mainColor varchar,
secondColor varchar
);
CREATE TABLE User
(
id integer PRIMARY KEY AUTOINCREMENT
);
CREATE TABLE FormEntries(name varchar, description varchar); CREATE TABLE Member
CREATE TABLE AccountUser( (
username varchar, idTeam integer,
hash varchar, idMember integer,
email varchar unique role char(1) CHECK (role IN ('C', 'P')),
FOREIGN KEY (idTeam) REFERENCES Team (id),
FOREIGN KEY (idMember) REFERENCES User (id)
); );
CREATE TABLE TacticInfo( CREATE TABLE TacticInfo
(
id integer PRIMARY KEY AUTOINCREMENT, id integer PRIMARY KEY AUTOINCREMENT,
name varchar, name varchar,
creation_date timestamp DEFAULT CURRENT_TIMESTAMP creation_date timestamp DEFAULT CURRENT_TIMESTAMP

@ -0,0 +1,39 @@
<?php
namespace App\Controller\Api;
use App\Controller\Control;
use App\Http\HttpRequest;
use App\Http\HttpResponse;
use App\Http\JsonHttpResponse;
use App\Model\AuthModel;
use App\Validation\Validators;
class APIAuthController {
private AuthModel $model;
/**
* @param AuthModel $model
*/
public function __construct(AuthModel $model) {
$this->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);
}
}

@ -3,6 +3,7 @@
namespace App\Controller\Api; namespace App\Controller\Api;
use App\Controller\Control; use App\Controller\Control;
use App\Data\Account;
use App\Http\HttpCodes; use App\Http\HttpCodes;
use App\Http\HttpRequest; use App\Http\HttpRequest;
use App\Http\HttpResponse; use App\Http\HttpResponse;
@ -23,26 +24,33 @@ class APITacticController {
$this->model = $model; $this->model = $model;
} }
public function updateName(int $tactic_id): HttpResponse { public function updateName(int $tactic_id, Account $account): HttpResponse {
return Control::runChecked([ return Control::runChecked([
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()],
], function (HttpRequest $request) use ($tactic_id) { ], function (HttpRequest $request) use ($tactic_id, $account) {
$this->model->updateName($tactic_id, $request["name"]);
$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); return HttpResponse::fromCode(HttpCodes::OK);
}, true); }, true);
} }
public function newTactic(): HttpResponse { public function newTactic(Account $account): HttpResponse {
return Control::runChecked([ return Control::runChecked([
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()],
], function (HttpRequest $request) { ], function (HttpRequest $request) use ($account) {
$tactic = $this->model->makeNew($request["name"]); $tactic = $this->model->makeNew($request["name"], $account->getId());
$id = $tactic->getId(); $id = $tactic->getId();
return new JsonHttpResponse(["id" => $id]); return new JsonHttpResponse(["id" => $id]);
}, true); }, true);
} }
public function getTacticInfo(int $id): HttpResponse { public function getTacticInfo(int $id, Account $account): HttpResponse {
$tactic_info = $this->model->get($id); $tactic_info = $this->model->get($id);
if ($tactic_info == null) { if ($tactic_info == null) {

@ -1,26 +0,0 @@
<?php
namespace App\Controller;
require_once __DIR__ . "/../react-display.php";
use App\Validation\ValidationFail;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
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) {
echo "Twig error: $e";
}
}
}

@ -7,6 +7,8 @@ use App\Http\HttpCodes;
use App\Http\HttpResponse; use App\Http\HttpResponse;
use App\Http\JsonHttpResponse; use App\Http\JsonHttpResponse;
use App\Http\ViewHttpResponse; use App\Http\ViewHttpResponse;
use App\Session\MutableSessionHandle;
use App\Validation\ValidationFail;
use Exception; use Exception;
use Twig\Environment; use Twig\Environment;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
@ -16,24 +18,35 @@ use Twig\Loader\FilesystemLoader;
class FrontController { class FrontController {
private AltoRouter $router; private AltoRouter $router;
private string $basePath;
private const USER_CONTROLLER = "UserController";
private const VISITOR_CONTROLLER = "VisitorController";
public function __construct(string $basePath) { public function __construct(string $basePath) {
$this->router = $this->createRouter($basePath); $this->router = $this->createRouter($basePath);
$this->initializeRouterMap(); $this->initializeRouterMap();
$this->basePath = $basePath;
} }
/** /**
* Main behavior of the FrontController * @param MutableSessionHandle $session
*
* @return void * @return void
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/ */
public function run(): void { public function run(MutableSessionHandle $session): void {
$match = $this->router->match(); $match = $this->router->match();
if ($match != null) { if ($match) {
$this->handleMatch($match); $this->handleMatch($match, $session);
} else { return;
$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));
} }
/** /**
@ -54,40 +67,53 @@ class FrontController {
* @return void * @return void
*/ */
private function initializeRouterMap(): void { private function initializeRouterMap(): void {
$this->router->map("GET", "/", "UserController"); $this->router->map("GET", "/home", self::USER_CONTROLLER);
$this->router->map("GET", "/[a:action]?", "UserController"); $this->router->map("GET|POST", "/user/[a:action]/[i:idTactic]?", self::USER_CONTROLLER);
$this->router->map("GET", "/tactic/[a:action]/[i:idTactic]?", "EditorController"); $this->router->map("GET|POST", "/visitor/[a:action]", self::VISITOR_CONTROLLER);
} }
/** /**
* @param array<string, mixed> $match * @param array<string, mixed> $match
* @param MutableSessionHandle $session
* @return void * @return void
*/ */
private function handleMatch(array $match): void { private function handleMatch(array $match, MutableSessionHandle $session): void {
$tag = $match['target']; $tag = $match['target'];
$action = $this->getAction($match); $action = $this->getAction($match);
$params = $match["params"]; $params = $match["params"];
unset($params['action']); 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 $controllerName
* @param string $action * @param string $action
* @param array<int, mixed> $params * @param array<int, mixed> $params
* @param MutableSessionHandle $session
* @return HttpResponse * @return HttpResponse
*/ */
private function tryToCall(string $controller, string $action, array $params): HttpResponse { private function tryToCall(string $controllerName, string $action, array $params, MutableSessionHandle $session): HttpResponse {
$controller = $this->getController($controller); if ($controllerName != self::VISITOR_CONTROLLER) {
try { $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");
}
}
$controller = $this->getController($controllerName);
if (is_callable([$controller, $action])) { 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); return call_user_func_array([$controller, $action], $params);
} else { } else {
return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); return ViewHttpResponse::twig("error.html.twig", [
} 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")],
} catch (Exception $e) { ], HttpCodes::NOT_FOUND);
return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND);
} }
} }
@ -101,7 +127,7 @@ class FrontController {
if (isset($match["params"]["action"])) { if (isset($match["params"]["action"])) {
return $match["params"]["action"]; return $match["params"]["action"];
} }
return "default"; return "home";
} }
/** /**
@ -124,6 +150,11 @@ class FrontController {
*/ */
private function handleResponseByType(HttpResponse $response): void { private function handleResponseByType(HttpResponse $response): void {
http_response_code($response->getCode()); http_response_code($response->getCode());
foreach ($response->getHeaders() as $header => $value) {
header("$header: $value");
}
if ($response instanceof ViewHttpResponse) { if ($response instanceof ViewHttpResponse) {
$this->displayViewByKind($response); $this->displayViewByKind($response);
} elseif ($response instanceof JsonHttpResponse) { } elseif ($response instanceof JsonHttpResponse) {
@ -151,7 +182,7 @@ class FrontController {
$loader = new FilesystemLoader('../src/Views/'); $loader = new FilesystemLoader('../src/Views/');
$twig = new Environment($loader); $twig = new Environment($loader);
$twig->display($file, $args); $twig->display($file, $args);
} catch (RuntimeError|SyntaxError|LoaderError $e) { } catch (RuntimeError | SyntaxError | LoaderError $e) {
http_response_code(500); http_response_code(500);
echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); 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;

@ -1,16 +1,15 @@
<?php <?php
namespace App\Controller; namespace App\Controller\Sub;
use App\Gateway\AuthGateway;
use App\Http\HttpRequest; use App\Http\HttpRequest;
use App\Http\HttpResponse; use App\Http\HttpResponse;
use App\Http\ViewHttpResponse; use App\Http\ViewHttpResponse;
use App\Model\AuthModel; use App\Model\AuthModel;
use App\Session\MutableSessionHandle;
use App\Validation\FieldValidationFail; use App\Validation\FieldValidationFail;
use App\Validation\ValidationFail; use App\Validation\ValidationFail;
use App\Validation\Validators; use App\Validation\Validators;
use Twig\Environment;
class AuthController { class AuthController {
private AuthModel $model; private AuthModel $model;
@ -43,27 +42,31 @@ class AuthController {
/** /**
* @param mixed[] $request * @param mixed[] $request
* @param MutableSessionHandle $session
* @return HttpResponse * @return HttpResponse
*/ */
public function confirmRegister(array $request): HttpResponse { public function confirmRegister(array $request, MutableSessionHandle $session): HttpResponse {
$fails = []; $fails = [];
$request = HttpRequest::from($request, $fails, [ $request = HttpRequest::from($request, $fails, [
"username" => [Validators::name(), Validators::lenBetween(2, 32)], "username" => [Validators::name(), Validators::lenBetween(2, 32)],
"password" => [Validators::lenBetween(6, 256)], "password" => [Validators::lenBetween(6, 256)],
"confirmpassword" => [Validators::lenBetween(6, 256)], "confirmpassword" => [Validators::lenBetween(6, 256)],
"email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)], "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)],
]); ]);
if (!empty($fails)) { if (!empty($fails)) {
return $this->displayBadFields("display_register.html.twig", $fails); return $this->displayBadFields("display_register.html.twig", $fails);
} }
$fails = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email']); $account = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email'], $fails);
if (empty($fails)) { 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); return $this->displayBadFields("display_register.html.twig", $fails);
} }
$session->setAccount($account);
$target_url = $session->getInitialTarget();
return HttpResponse::redirect($target_url ?? "/home");
}
public function displayLogin(): HttpResponse { public function displayLogin(): HttpResponse {
return ViewHttpResponse::twig("display_login.html.twig", []); return ViewHttpResponse::twig("display_login.html.twig", []);
@ -73,22 +76,26 @@ class AuthController {
* @param mixed[] $request * @param mixed[] $request
* @return HttpResponse * @return HttpResponse
*/ */
public function confirmLogin(array $request): HttpResponse { public function confirmLogin(array $request, MutableSessionHandle $session): HttpResponse {
$fails = []; $fails = [];
$request = HttpRequest::from($request, $fails, [ $request = HttpRequest::from($request, $fails, [
"password" => [Validators::lenBetween(6, 256)], "password" => [Validators::lenBetween(6, 256)],
"email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)], "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)],
]); ]);
if (!empty($fails)) { if (!empty($fails)) {
return $this->displayBadFields("display_login.html.twig", $fails); return $this->displayBadFields("display_login.html.twig", $fails);
} }
$fails = $this->model->login($request['email'], $request['password']); $account = $this->model->login($request['email'], $request['password'], $fails);
if (empty($fails)) { 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); return $this->displayBadFields("display_login.html.twig", $fails);
} }
$session->setAccount($account);
$target_url = $session->getInitialTarget();
$session->setInitialTarget(null);
return HttpResponse::redirect($target_url ?? "/home");
}
} }

@ -1,8 +1,9 @@
<?php <?php
namespace App\Controller; namespace App\Controller\Sub;
use App\Connexion; use App\Connexion;
use App\Controller\VisitorController;
use App\Data\TacticInfo; use App\Data\TacticInfo;
use App\Gateway\TacticInfoGateway; use App\Gateway\TacticInfoGateway;
use App\Http\HttpCodes; use App\Http\HttpCodes;
@ -10,36 +11,43 @@ use App\Http\HttpResponse;
use App\Http\JsonHttpResponse; use App\Http\JsonHttpResponse;
use App\Http\ViewHttpResponse; use App\Http\ViewHttpResponse;
use App\Model\TacticModel; use App\Model\TacticModel;
use App\Session\SessionHandle;
use App\Validation\ValidationFail;
use App\Validator\TacticValidator;
class EditorController { class EditorController {
private TacticModel $model; private TacticModel $model;
public function __construct() { public function __construct(TacticModel $model) {
$this->model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); $this->model = $model;
} }
private function openEditor(TacticInfo $tactic): HttpResponse { private function openEditor(TacticInfo $tactic): HttpResponse {
return ViewHttpResponse::react("views/Editor.tsx", ["name" => $tactic->getName(), "id" => $tactic->getId()]); return ViewHttpResponse::react("views/Editor.tsx", ["name" => $tactic->getName(), "id" => $tactic->getId()]);
} }
public function create(): HttpResponse { public function createNew(SessionHandle $session): HttpResponse {
$tactic = $this->model->makeNewDefault(); $tactic = $this->model->makeNewDefault($session->getAccount()->getId());
return $this->openEditor($tactic); return $this->openEditor($tactic);
} }
/** /**
* returns an editor view for a given tactic * returns an editor view for a given tactic
* @param int $id the targeted tactic identifier * @param int $id the targeted tactic identifier
* @param SessionHandle $session
* @return HttpResponse * @return HttpResponse
*/ */
public function edit(int $id): HttpResponse { public function edit(int $id, SessionHandle $session): HttpResponse {
$tactic = $this->model->get($id); $tactic = $this->model->get($id);
if ($tactic == null) { $failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId());
return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND);
if ($failure != null) {
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);
} }
return $this->openEditor($tactic); return $this->openEditor($tactic);
} }
} }

@ -0,0 +1,83 @@
<?php
namespace App\Controller\Sub;
use App\Http\HttpRequest;
use App\Http\HttpResponse;
use App\Http\ViewHttpResponse;
use App\Model\TeamModel;
use App\Validation\FieldValidationFail;
use App\Validation\Validators;
class TeamController {
private TeamModel $model;
/**
* @param TeamModel $model
*/
public function __construct(TeamModel $model) {
$this->model = $model;
}
public function displaySubmitTeam(): HttpResponse {
return ViewHttpResponse::twig("insert_team.html.twig", []);
}
/**
* @param array<string, mixed> $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<string , mixed> $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]);
}
}

@ -1,12 +1,15 @@
<?php <?php
namespace App\Controller; namespace App\Controller\Sub;
use App\Http\HttpCodes; use App\Http\HttpCodes;
use App\Http\HttpResponse; use App\Http\HttpResponse;
use App\Http\JsonHttpResponse; use App\Http\JsonHttpResponse;
use App\Http\ViewHttpResponse; use App\Http\ViewHttpResponse;
use App\Model\TacticModel; use App\Model\TacticModel;
use App\Session\SessionHandle;
use App\Validation\ValidationFail;
use App\Validator\TacticValidator;
class VisualizerController { class VisualizerController {
private TacticModel $tacticModel; private TacticModel $tacticModel;
@ -19,14 +22,15 @@ class VisualizerController {
$this->tacticModel = $tacticModel; $this->tacticModel = $tacticModel;
} }
public function openVisualizer(int $id): HttpResponse { public function visualize(int $id, SessionHandle $session): HttpResponse {
$tactic = $this->tacticModel->get($id); $tactic = $this->tacticModel->get($id);
if ($tactic == null) { $failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId());
return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND);
if ($failure != null) {
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);
} }
return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]); return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]);
} }
} }

@ -9,8 +9,11 @@ use App\Http\HttpResponse;
use App\Http\ViewHttpResponse; use App\Http\ViewHttpResponse;
use App\Model\AuthModel; use App\Model\AuthModel;
use App\Model\TacticModel; use App\Model\TacticModel;
use App\Session\SessionHandle;
use App\Model\TeamModel;
use App\Gateway\TeamGateway;
class UserController { class UserController extends VisitorController{
private TacticModel $tacticMdl; private TacticModel $tacticMdl;
public function __construct() public function __construct()
@ -24,7 +27,47 @@ class UserController {
return ViewHttpResponse::twig("home.twig", ["recentTactic" => $listTactic]); return ViewHttpResponse::twig("home.twig", ["recentTactic" => $listTactic]);
} }
public function default(): HttpResponse { public function view(int $id, SessionHandle $session): HttpResponse {
return self::home(); $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 Sub\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);
}
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);
}
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);
} }
} }

@ -0,0 +1,28 @@
<?php
namespace App\Controller;
use App\Connexion;
use App\Gateway\AccountGateway;
use App\Http\HttpResponse;
use App\Model\AuthModel;
use App\Session\MutableSessionHandle;
class VisitorController {
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();
}
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);
}
}

@ -4,8 +4,6 @@ namespace App\Data;
use http\Exception\InvalidArgumentException; use http\Exception\InvalidArgumentException;
const PHONE_NUMBER_REGEXP = "/^\\+[0-9]+$/";
/** /**
* Base class of a user account. * Base class of a user account.
* Contains the private information that we don't want * Contains the private information that we don't want
@ -16,90 +14,50 @@ class Account {
* @var string $email account's mail address * @var string $email account's mail address
*/ */
private string $email; 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; private string $name;
/** /**
* @var int account's unique identifier * @var int
*/ */
private int $id; private int $id;
/** /**
* @param string $email * @param string $email
* @param string $phoneNumber * @param string $name
* @param AccountUser $user * @param string $token
* @param Team[] $teams
* @param int $id * @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, int $id) {
$this->email = $email; $this->email = $email;
$this->phoneNumber = $phoneNumber; $this->name = $name;
$this->user = $user; $this->token = $token;
$this->teams = $teams;
$this->id = $id; $this->id = $id;
} }
/** public function getId(): int {
* @return string return $this->id;
*/
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 getEmail(): string {
* @param string $phoneNumber return $this->email;
*/
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 { public function getToken(): string {
return $this->id; return $this->token;
} }
/** public function getName(): string {
* @return Team[] return $this->name;
*/
public function getTeams(): array {
return $this->teams;
} }
public function getUser(): AccountUser {
return $this->user;
}
} }

@ -2,29 +2,46 @@
namespace App\Data; namespace App\Data;
use http\Exception\InvalidArgumentException; use InvalidArgumentException;
class Color { class Color {
/** /**
* @var int 6 bytes unsigned int that represents an RGB color * @var string that represents an hexadecimal color code
*/ */
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 * @throws InvalidArgumentException if the value is negative or greater than 0xFFFFFF
*/ */
public function __construct(int $value) {
private function __construct(string $value) {
if ($value < 0 || $value > 0xFFFFFF) { if ($value < 0 || $value > 0xFFFFFF) {
throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF"); throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF");
} }
$this->value = $value; $this->hex = $value;
} }
/** /**
* @return int * @return string
*/ */
public function getValue(): int { public function getValue(): string {
return $this->value; return $this->hex;
}
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;
}
public static function tryFrom(string $value): ?Color {
if (!preg_match('/#(?:[0-9a-fA-F]{6})/', $value)) {
return null;
}
return new Color($value);
} }
} }

@ -25,6 +25,7 @@ class Member {
$this->role = $role; $this->role = $role;
} }
/** /**
* @return int * @return int
*/ */
@ -38,5 +39,4 @@ class Member {
public function getRole(): MemberRole { public function getRole(): MemberRole {
return $this->role; return $this->role;
} }
} }

@ -24,6 +24,14 @@ final class MemberRole {
$this->value = $val; $this->value = $val;
} }
public static function player(): MemberRole {
return new MemberRole(MemberRole::ROLE_PLAYER);
}
public static function coach(): MemberRole {
return new MemberRole(MemberRole::ROLE_COACH);
}
private function isValid(int $val): bool { private function isValid(int $val): bool {
return ($val <= self::MAX and $val >= self::MIN); return ($val <= self::MAX and $val >= self::MIN);
} }

@ -7,14 +7,18 @@ class TacticInfo implements \JsonSerializable {
private string $name; private string $name;
private int $creation_date; private int $creation_date;
private int $ownerId;
/** /**
* @param int $id * @param int $id
* @param string $name * @param string $name
* @param int $creation_date * @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->id = $id;
$this->name = $name; $this->name = $name;
$this->ownerId = $ownerId;
$this->creation_date = $creation_date; $this->creation_date = $creation_date;
} }
@ -26,6 +30,13 @@ class TacticInfo implements \JsonSerializable {
return $this->name; return $this->name;
} }
/**
* @return int
*/
public function getOwnerId(): int {
return $this->ownerId;
}
public function getCreationTimestamp(): int { public function getCreationTimestamp(): int {
return $this->creation_date; return $this->creation_date;
} }

@ -2,11 +2,10 @@
namespace App\Data; namespace App\Data;
use http\Url;
class Team { class Team {
private int $id;
private string $name; private string $name;
private Url $picture; private string $picture;
private Color $mainColor; private Color $mainColor;
private Color $secondColor; private Color $secondColor;
@ -17,12 +16,13 @@ class Team {
/** /**
* @param string $name * @param string $name
* @param Url $picture * @param string $picture
* @param Color $mainColor * @param Color $mainColor
* @param Color $secondColor * @param Color $secondColor
* @param Member[] $members * @param Member[] $members
*/ */
public function __construct(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->name = $name;
$this->picture = $picture; $this->picture = $picture;
$this->mainColor = $mainColor; $this->mainColor = $mainColor;
@ -30,6 +30,13 @@ class Team {
$this->members = $members; $this->members = $members;
} }
/**
* @return int
*/
public function getId(): int {
return $this->id;
}
/** /**
* @return string * @return string
*/ */
@ -38,9 +45,9 @@ class Team {
} }
/** /**
* @return Url * @return string
*/ */
public function getPicture(): Url { public function getPicture(): string {
return $this->picture; return $this->picture;
} }

@ -0,0 +1,74 @@
<?php
namespace App\Gateway;
use App\Connexion;
use App\Data\Account;
use PDO;
class AccountGateway {
private Connexion $con;
/**
* @param Connexion $con
*/
public function __construct(Connexion $con) {
$this->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<string, mixed>|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 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;
}
return new Account($acc["email"], $acc["username"], $acc["token"], $acc["id"]);
}
}

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

@ -18,7 +18,7 @@ class TacticInfoGateway {
public function get(int $id): ?TacticInfo { public function get(int $id): ?TacticInfo {
$res = $this->con->fetch( $res = $this->con->fetch(
"SELECT * FROM TacticInfo WHERE id = :id", "SELECT * FROM Tactic WHERE id = :id",
[":id" => [$id, PDO::PARAM_INT]] [":id" => [$id, PDO::PARAM_INT]]
); );
@ -28,9 +28,10 @@ class TacticInfoGateway {
$row = $res[0]; $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"]);
} }
/** /**
* Return the nb last tactics created * Return the nb last tactics created
* *
@ -39,7 +40,7 @@ class TacticInfoGateway {
*/ */
public function getLast(int $nb) : ?array { public function getLast(int $nb) : ?array {
$res = $this->con->fetch( $res = $this->con->fetch(
"SELECT * FROM TacticInfo ORDER BY creation_date DESC LIMIT :nb ", "SELECT * FROM Tactic ORDER BY creation_date DESC LIMIT :nb ",
[":nb" => [$nb, PDO::PARAM_INT]] [":nb" => [$nb, PDO::PARAM_INT]]
); );
if (count($res) == 0) { if (count($res) == 0) {
@ -48,21 +49,24 @@ class TacticInfoGateway {
return $res; return $res;
} }
public function insert(string $name): TacticInfo { public function insert(string $name, int $owner): TacticInfo {
$this->con->exec( $this->con->exec(
"INSERT INTO TacticInfo(name) VALUES(:name)", "INSERT INTO Tactic(name, owner) VALUES(:name, :owner)",
[":name" => [$name, PDO::PARAM_STR]] [
":name" => [$name, PDO::PARAM_STR],
":owner" => [$owner, PDO::PARAM_INT],
]
); );
$row = $this->con->fetch( $row = $this->con->fetch(
"SELECT id, creation_date FROM TacticInfo WHERE :id = id", "SELECT id, creation_date, owner FROM Tactic WHERE :id = id",
[':id' => [$this->con->lastInsertId(), PDO::PARAM_INT]] [':id' => [$this->con->lastInsertId(), PDO::PARAM_INT]]
)[0]; )[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 { public function updateName(int $id, string $name): void {
$this->con->exec( $this->con->exec(
"UPDATE TacticInfo SET name = :name WHERE id = :id", "UPDATE Tactic SET name = :name WHERE id = :id",
[ [
":name" => [$name, PDO::PARAM_STR], ":name" => [$name, PDO::PARAM_STR],
":id" => [$id, PDO::PARAM_INT], ":id" => [$id, PDO::PARAM_INT],

@ -0,0 +1,79 @@
<?php
namespace App\Gateway;
use App\Connexion;
use PDO;
class TeamGateway {
private Connexion $con;
public function __construct(Connexion $con) {
$this->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<string,mixed>[]
*/
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<string,mixed>[]
*/
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<string,int>[]
*/
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<string,mixed>[]
*/
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],
]
);
}
}

@ -7,7 +7,11 @@ namespace App\Http;
*/ */
class HttpCodes { class HttpCodes {
public const OK = 200; public const OK = 200;
public const FOUND = 302;
public const BAD_REQUEST = 400; public const BAD_REQUEST = 400;
public const FORBIDDEN = 403;
public const NOT_FOUND = 404; public const NOT_FOUND = 404;
} }

@ -3,21 +3,41 @@
namespace App\Http; namespace App\Http;
class HttpResponse { class HttpResponse {
/**
* @var array<string, string>
*/
private array $headers;
private int $code; private int $code;
/** /**
* @param int $code * @param int $code
* @param array<string, string> $headers
*/ */
public function __construct(int $code) { public function __construct(int $code, array $headers) {
$this->code = $code; $this->code = $code;
$this->headers = $headers;
} }
public function getCode(): int { public function getCode(): int {
return $this->code; return $this->code;
} }
/**
* @return array<string, string>
*/
public function getHeaders(): array {
return $this->headers;
}
public static function fromCode(int $code): HttpResponse { 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]);
} }
} }

@ -12,7 +12,7 @@ class JsonHttpResponse extends HttpResponse {
* @param mixed $payload * @param mixed $payload
*/ */
public function __construct($payload, int $code = HttpCodes::OK) { public function __construct($payload, int $code = HttpCodes::OK) {
parent::__construct($code); parent::__construct($code, []);
$this->payload = $payload; $this->payload = $payload;
} }

@ -26,7 +26,7 @@ class ViewHttpResponse extends HttpResponse {
* @param array<string, mixed> $arguments * @param array<string, mixed> $arguments
*/ */
private function __construct(int $kind, string $file, array $arguments, int $code = HttpCodes::OK) { private function __construct(int $kind, string $file, array $arguments, int $code = HttpCodes::OK) {
parent::__construct($code); parent::__construct($code, []);
$this->kind = $kind; $this->kind = $kind;
$this->file = $file; $this->file = $file;
$this->arguments = $arguments; $this->arguments = $arguments;

@ -2,17 +2,18 @@
namespace App\Model; namespace App\Model;
use App\Controller\AuthController; use App\Data\Account;
use App\Gateway\AuthGateway; use App\Gateway\AccountGateway;
use App\Validation\FieldValidationFail; use App\Validation\FieldValidationFail;
use App\Validation\ValidationFail; use App\Validation\ValidationFail;
class AuthModel { 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; $this->gateway = $gateway;
} }
@ -22,59 +23,54 @@ class AuthModel {
* @param string $password * @param string $password
* @param string $confirmPassword * @param string $confirmPassword
* @param string $email * @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 { public function register(string $username, string $password, string $confirmPassword, string $email, array &$failures): ?Account {
$errors = [];
if ($password != $confirmPassword) { 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)) { if ($this->gateway->exists($email)) {
$errors[] = new FieldValidationFail("email", "email already exist"); $failures[] = new FieldValidationFail("email", "email already exist");
} }
if(empty($errors)) { if (!empty($failures)) {
$hash = password_hash($password, PASSWORD_DEFAULT); return null;
$this->gateway->insertAccount($username, $hash, $email);
} }
return $errors; $hash = password_hash($password, PASSWORD_DEFAULT);
}
/** $token = $this->generateToken();
* @param string $email $accountId = $this->gateway->insertAccount($username, $email, $token, $hash);
* @return array<string,string>|null return new Account($email, $username, $token, $accountId);
*/
public function getUserFields(string $email): ?array {
return $this->gateway->getUserFields($email);
} }
private function generateToken(): string {
return base64_encode(random_bytes(64));
}
/** /**
* @param string $email * @param string $email
* @param string $password * @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 { public function login(string $email, string $password, array &$failures): ?Account {
$errors = []; if (!$this->gateway->exists($email)) {
$failures[] = new FieldValidationFail("email", "email doesnt exists");
if (!$this->gateway->mailExist($email)) { return null;
$errors[] = new FieldValidationFail("email", "email doesnt exists");
return $errors;
} }
$hash = $this->gateway->getUserHash($email); $hash = $this->gateway->getHash($email);
if (!password_verify($password, $hash)) { if (!password_verify($password, $hash)) {
$errors[] = new FieldValidationFail("password", "invalid password"); $failures[] = new FieldValidationFail("password", "invalid password");
return null;
} }
return $errors; return $this->gateway->getAccountFromMail($email);
} }
} }

@ -2,8 +2,10 @@
namespace App\Model; namespace App\Model;
use App\Data\Account;
use App\Data\TacticInfo; use App\Data\TacticInfo;
use App\Gateway\TacticInfoGateway; use App\Gateway\TacticInfoGateway;
use App\Validation\ValidationFail;
class TacticModel { class TacticModel {
public const TACTIC_DEFAULT_NAME = "Nouvelle tactique"; public const TACTIC_DEFAULT_NAME = "Nouvelle tactique";
@ -18,12 +20,12 @@ class TacticModel {
$this->tactics = $tactics; $this->tactics = $tactics;
} }
public function makeNew(string $name): TacticInfo { public function makeNew(string $name, int $ownerId): TacticInfo {
return $this->tactics->insert($name); return $this->tactics->insert($name, $ownerId);
} }
public function makeNewDefault(): ?TacticInfo { public function makeNewDefault(int $ownerId): ?TacticInfo {
return $this->tactics->insert(self::TACTIC_DEFAULT_NAME); return $this->tactics->insert(self::TACTIC_DEFAULT_NAME, $ownerId);
} }
/** /**
@ -49,15 +51,22 @@ class TacticModel {
* Update the name of a tactic * Update the name of a tactic
* @param int $id the tactic identifier * @param int $id the tactic identifier
* @param string $name the new name to set * @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 { public function updateName(int $id, string $name, int $authId): array {
if ($this->tactics->get($id) == null) {
return false; $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); $this->tactics->updateName($id, $name);
return true; return [];
} }
} }

@ -0,0 +1,54 @@
<?php
namespace App\Model;
use App\Gateway\TeamGateway;
use App\Data\Team;
use App\Data\Member;
use App\Data\MemberRole;
use App\Data\Color;
class TeamModel {
private TeamGateway $gateway;
/**
* @param TeamGateway $gateway
*/
public function __construct(TeamGateway $gateway) {
$this->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);
}
}

@ -0,0 +1,20 @@
<?php
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;
}

@ -0,0 +1,34 @@
<?php
namespace App\Session;
use App\Data\Account;
/**
* A PHP session handle
*/
class PhpSessionHandle implements MutableSessionHandle {
public static function init(): self {
if (session_status() !== PHP_SESSION_NONE) {
throw new \Exception("A php session is already started !");
}
session_start();
return new PhpSessionHandle();
}
public function getAccount(): ?Account {
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;
}
}

@ -0,0 +1,23 @@
<?php
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;
}

@ -34,7 +34,11 @@ class ValidationFail implements JsonSerializable {
} }
public static function notFound(string $message): ValidationFail { public static function notFound(string $message): ValidationFail {
return new ValidationFail("not found", $message); return new ValidationFail("Not found", $message);
}
public static function unauthorized(string $message = "Unauthorized"): ValidationFail {
return new ValidationFail("Unauthorized", $message);
} }
} }

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

@ -0,0 +1,21 @@
<?php
namespace App\Validator;
use App\Data\TacticInfo;
use App\Validation\ValidationFail;
class TacticValidator {
public static function validateAccess(?TacticInfo $tactic, int $tacticId, int $ownerId): ?ValidationFail {
if ($tactic == null) {
return ValidationFail::notFound("La tactique $tacticId n'existe pas");
}
if ($tactic->getOwnerId() != $ownerId) {
return ValidationFail::unauthorized("Vous ne pouvez pas accéder à cette tactique.");
}
return null;
}
}

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Twig view</title>
<style>
body{
background-color: #f1f1f1;
display: flex;
flex-direction: column;
align-items: center;
}
section{
width: 60%;
}
.square{
width:50px;
height:50px;
}
#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{
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
}
.team{
border-color: darkgrey;
border-radius: 20px;
}
.color{
flex-direction: row;
justify-content: space-between;
}
.logo{
height: 80px;
width: 80px;
}
</style>
</head>
<body>
<header>
<h1><a href="/">IQBall</a></h1>
</header>
<section class="container">
<div class="team container">
<div>
<h1>{{ team.name }}</h1>
<img src="{{ team.picture }}" alt="Logo d'équipe" class="logo">
</div>
<div>
<div class="color"><p>Couleur principale : </p><div class="square" id="mainColor"></div> </div>
<div class="color"><p>Couleur secondaire : </p><div class="square" id="secondColor"></div></div>
</div>
{% for m in team.members %}
<p> m.id </p>
{% endfor %}
</div>
</section>
</body>
</html>

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Twig view</title>
</head>
<body>
{% if teams is empty %}
<p>Aucune équipe n'a été trouvée</p>
<div class="container">
<h2>Chercher une équipe</h2>
<form action="/listTea" method="post">
<div class="form-group">
<label for="name">Nom de l'équipe :</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<input type="submit" value="Confirmer">
</div>
</form>
</div>
{% else %}
{% for t in teams %}
<div class="team" onclick="window.location.href = '/getTeam/{{ t.id }}'">
<p>Nom de l'équipe : {{ t.name }}</p>
<img src="{{ t.picture }}" alt="logo de l'équipe">
</div>
{% endfor %}
{% endif %}
</body>
</html>

@ -51,7 +51,7 @@
{% endfor %} {% endfor %}
<button class="button" onclick="location.href='/'" type="button">Retour à la page d'accueil</button> <button class="button" onclick="location.href='/home'" type="button">Retour à la page d'accueil</button>
</body> </body>
</html> </html>

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Insertion view</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f1f1f1;
}
.container {
max-width: 400px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
{% for item in bad_fields %}
#{{ item }}{
border-color: red;
}
{% endfor %}
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="submit"] {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<h2>Créer une équipe</h2>
<form action="/createTeam" method="post">
<div class="form-group">
<label for="name">Nom de l'équipe :</label>
<input type="text" id="name" name="name" required>
<label for= "picture">Logo:</label>
<input type="text" id="picture" name="picture" required >
<label for="mainColor">Couleur principale</label>
<input type="color" id="mainColor" name="mainColor" required>
<label for="secondColor">Couleur secondaire</label>
<input type="color" id="secondColor" name="secondColor" required>
</div>
<div class="form-group">
<input type="submit" value="Confirmer">
</div>
</form>
</div>
</body>
</html>

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Insertion view</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f1f1f1;
}
.container {
max-width: 400px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
{% for item in bad_fields %}
#{{ item }}{
border-color: red;
}
{% endfor %}
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
input[type="submit"] {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<h2>Chercher une équipe</h2>
<form action="/listTeams" method="post">
<div class="form-group">
<label for="name">Nom de l'équipe :</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<input type="submit" value="Confirmer">
</div>
</form>
</div>
</body>
</html>
Loading…
Cancel
Save