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/models.puml b/Documentation/models.puml index d95343c..82037bc 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -81,7 +81,7 @@ AuthController --> "- model" AuthModel class AuthModel{ + register(username : string, password : string, confirmPassword : string, email : string): array - + getUserFields(email : string):array + + getAccount(email : string):array + login(email : string, password : string) } AuthModel --> "- gateway" AuthGateway @@ -89,9 +89,9 @@ AuthModel --> "- gateway" AuthGateway class AuthGateway{ -con : Connection - + mailExist(email : string) : bool + + mailExists(email : string) : bool + insertAccount(username : string, hash : string, email : string) - + getUserHash(email : string):string - + getUserFields (email : string): array + + getHash(email : string):string + + getAccount (email : string): array } @enduml \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon index bc6c041..715fe84 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,5 +9,7 @@ parameters: - sql/database.php - profiles/dev-config-profile.php - profiles/prod-config-profile.php + - public/api/index.php excludePaths: - src/react-display-file.php + - public/api/index.php diff --git a/public/api/index.php b/public/api/index.php index 3ed5caa..c5f8f2e 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -6,32 +6,137 @@ require "../../sql/database.php"; require "../utils.php"; use App\Connexion; +use App\Controller\Api\APIAuthController; use App\Controller\Api\APITacticController; +use App\Data\Account; +use App\Gateway\AccountGateway; use App\Gateway\TacticInfoGateway; +use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; +use App\Model\AuthModel; use App\Model\TacticModel; +use App\Session\PhpSessionHandle; +use App\Validation\ValidationFail; -$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->setBasePath(get_public_path() . "/api"); -$tacticEndpoint = new APITacticController(new TacticModel(new TacticInfoGateway($con))); -$router->map("POST", "/tactic/[i:id]/edit/name", fn(int $id) => $tacticEndpoint->updateName($id)); -$router->map("GET", "/tactic/[i:id]", fn(int $id) => $tacticEndpoint->getTacticInfo($id)); -$router->map("POST", "/tactic/new", fn() => $tacticEndpoint->newTactic()); +$router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); +$router->map("GET", "/tactic/[i:id]", Action::auth(fn(int $id, Account $acc) => getTacticController()->getTacticInfo($id, $acc))); +$router->map("POST", "/tactic/new", Action::auth(fn(Account $acc) => getTacticController()->newTactic($acc))); +$router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize())); $match = $router->match(); -if ($match == null) { - echo "404 not found"; - header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); - exit(1); -} - -$response = call_user_func_array($match['target'], $match['params']); +$response = handleMatch($match); http_response_code($response->getCode()); if ($response instanceof JsonHttpResponse) { diff --git a/public/index.php b/public/index.php index f898d7f..cd4868d 100644 --- a/public/index.php +++ b/public/index.php @@ -1,5 +1,6 @@ run(); + +$frontController->run(PhpSessionHandle::init()); + 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..e105a1e 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -1,40 +1,53 @@ -- 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 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 FormEntries(name varchar, description varchar); -CREATE TABLE AccountUser( - username varchar, - hash varchar, - email varchar unique +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, +CREATE TABLE Team +( + id integer PRIMARY KEY AUTOINCREMENT, + name varchar, + picture varchar, + mainColor varchar, secondColor varchar ); -CREATE TABLE User( +CREATE TABLE User +( id integer PRIMARY KEY AUTOINCREMENT ); -CREATE TABLE Member( - idTeam integer, +CREATE TABLE Member +( + idTeam integer, idMember integer, - role char(1) CHECK (role IN ('C','P')), - FOREIGN KEY (idTeam) REFERENCES Team(id), - FOREIGN KEY (idMember) REFERENCES User(id) + role char(1) CHECK (role IN ('C', 'P')), + FOREIGN KEY (idTeam) REFERENCES Team (id), + FOREIGN KEY (idMember) REFERENCES User (id) ); -CREATE TABLE TacticInfo( - id integer PRIMARY KEY AUTOINCREMENT, - name varchar, +CREATE TABLE TacticInfo +( + id integer PRIMARY KEY AUTOINCREMENT, + name varchar, creation_date timestamp DEFAULT CURRENT_TIMESTAMP ); diff --git a/src/Controller/Api/APIAuthController.php b/src/Controller/Api/APIAuthController.php new file mode 100644 index 0000000..d9e4c23 --- /dev/null +++ b/src/Controller/Api/APIAuthController.php @@ -0,0 +1,39 @@ +model = $model; + } + + + public function authorize(): HttpResponse { + return Control::runChecked([ + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)], + "password" => [Validators::lenBetween(6, 256)], + ], function (HttpRequest $req) { + $failures = []; + $account = $this->model->login($req["email"], $req["password"], $failures); + + if (!empty($failures)) { + return new JsonHttpResponse($failures); + } + + return new JsonHttpResponse(["authorization" => $account->getToken()]); + }, true); + } + +} diff --git a/src/Controller/Api/APITacticController.php b/src/Controller/Api/APITacticController.php index ec0edc8..c1fcee3 100644 --- a/src/Controller/Api/APITacticController.php +++ b/src/Controller/Api/APITacticController.php @@ -3,6 +3,7 @@ namespace App\Controller\Api; use App\Controller\Control; +use App\Data\Account; use App\Http\HttpCodes; use App\Http\HttpRequest; use App\Http\HttpResponse; @@ -23,26 +24,33 @@ class APITacticController { $this->model = $model; } - public function updateName(int $tactic_id): HttpResponse { + public function updateName(int $tactic_id, Account $account): HttpResponse { return Control::runChecked([ "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], - ], function (HttpRequest $request) use ($tactic_id) { - $this->model->updateName($tactic_id, $request["name"]); + ], function (HttpRequest $request) use ($tactic_id, $account) { + + $failures = $this->model->updateName($tactic_id, $request["name"], $account->getId()); + + if (!empty($failures)) { + //TODO find a system to handle Unauthorized error codes more easily from failures. + return new JsonHttpResponse($failures, HttpCodes::BAD_REQUEST); + } + return HttpResponse::fromCode(HttpCodes::OK); }, true); } - public function newTactic(): HttpResponse { + public function newTactic(Account $account): HttpResponse { return Control::runChecked([ "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], - ], function (HttpRequest $request) { - $tactic = $this->model->makeNew($request["name"]); + ], function (HttpRequest $request) use ($account) { + $tactic = $this->model->makeNew($request["name"], $account->getId()); $id = $tactic->getId(); return new JsonHttpResponse(["id" => $id]); }, true); } - public function getTacticInfo(int $id): HttpResponse { + public function getTacticInfo(int $id, Account $account): HttpResponse { $tactic_info = $this->model->get($id); if ($tactic_info == null) { diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index eb796f7..d02bcbc 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -7,6 +7,8 @@ use App\Http\HttpCodes; use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; +use App\Session\MutableSessionHandle; +use App\Validation\ValidationFail; use Exception; use Twig\Environment; use Twig\Error\LoaderError; @@ -16,24 +18,35 @@ use Twig\Loader\FilesystemLoader; class FrontController { private AltoRouter $router; + private string $basePath; + + private const USER_CONTROLLER = "UserController"; + private const VISITOR_CONTROLLER = "VisitorController"; + public function __construct(string $basePath) { $this->router = $this->createRouter($basePath); $this->initializeRouterMap(); + $this->basePath = $basePath; } /** - * Main behavior of the FrontController - * + * @param MutableSessionHandle $session * @return void + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError */ - public function run(): void { + public function run(MutableSessionHandle $session): void { $match = $this->router->match(); - if ($match != null) { - $this->handleMatch($match); - } else { - $this->displayViewByKind(ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND)); + if ($match) { + $this->handleMatch($match, $session); + return; } + + $this->displayViewByKind(ViewHttpResponse::twig("error.html.twig", [ + 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")], + ], HttpCodes::NOT_FOUND)); } /** @@ -54,36 +67,53 @@ class FrontController { * @return void */ private function initializeRouterMap(): void { - $this->router->map("GET", "/", "UserController"); - $this->router->map("GET|POST", "/[a:action]?/[i:id]", "UserController"); - $this->router->map("GET|POST", "/tactic/[a:action]/[i:idTactic]?", "UserController"); + $this->router->map("GET", "/home", self::USER_CONTROLLER); + $this->router->map("GET|POST", "/user/[a:action]/[i:idTactic]?", self::USER_CONTROLLER); + $this->router->map("GET|POST", "/visitor/[a:action]", self::VISITOR_CONTROLLER); } /** * @param array $match + * @param MutableSessionHandle $session * @return void */ - private function handleMatch(array $match): void { + private function handleMatch(array $match, MutableSessionHandle $session): void { $tag = $match['target']; $action = $this->getAction($match); $params = $match["params"]; unset($params['action']); - $this->handleResponseByType($this->tryToCall($tag, $action, array_values($params))); + $this->handleResponseByType($this->tryToCall($tag, $action, array_values($params), $session)); } + /** - * @param string $controller + * @param string $controllerName * @param string $action * @param array $params + * @param MutableSessionHandle $session * @return HttpResponse */ - private function tryToCall(string $controller, string $action, array $params): HttpResponse { - $controller = $this->getController($controller); + private function tryToCall(string $controllerName, string $action, array $params, MutableSessionHandle $session): HttpResponse { + if ($controllerName != self::VISITOR_CONTROLLER) { + $account = $session->getAccount(); + if ($account == null) { + // put in the session the initial url the user wanted to get + $session->setInitialTarget($_SERVER['REQUEST_URI']); + return HttpResponse::redirect($this->basePath . "/visitor/login"); + } + } + + $controller = $this->getController($controllerName); + if (is_callable([$controller, $action])) { + // append the session as the last parameter of a controller function + $params[] = $session; return call_user_func_array([$controller, $action], $params); } else { - return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); + return ViewHttpResponse::twig("error.html.twig", [ + 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")], + ], HttpCodes::NOT_FOUND); } } @@ -97,7 +127,7 @@ class FrontController { if (isset($match["params"]["action"])) { return $match["params"]["action"]; } - return "default"; + return "home"; } /** @@ -120,6 +150,11 @@ class FrontController { */ private function handleResponseByType(HttpResponse $response): void { http_response_code($response->getCode()); + + foreach ($response->getHeaders() as $header => $value) { + header("$header: $value"); + } + if ($response instanceof ViewHttpResponse) { $this->displayViewByKind($response); } elseif ($response instanceof JsonHttpResponse) { diff --git a/src/Controller/Sub/AuthController.php b/src/Controller/Sub/AuthController.php index a78b02e..d94da43 100644 --- a/src/Controller/Sub/AuthController.php +++ b/src/Controller/Sub/AuthController.php @@ -6,6 +6,7 @@ use App\Http\HttpRequest; use App\Http\HttpResponse; use App\Http\ViewHttpResponse; use App\Model\AuthModel; +use App\Session\MutableSessionHandle; use App\Validation\FieldValidationFail; use App\Validation\ValidationFail; use App\Validation\Validators; @@ -41,25 +42,29 @@ class AuthController { /** * @param mixed[] $request + * @param MutableSessionHandle $session * @return HttpResponse */ - public function confirmRegister(array $request): HttpResponse { + public function confirmRegister(array $request, MutableSessionHandle $session): HttpResponse { $fails = []; $request = HttpRequest::from($request, $fails, [ "username" => [Validators::name(), Validators::lenBetween(2, 32)], "password" => [Validators::lenBetween(6, 256)], "confirmpassword" => [Validators::lenBetween(6, 256)], - "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)], ]); if (!empty($fails)) { return $this->displayBadFields("display_register.html.twig", $fails); } - $fails = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email']); - if (empty($fails)) { - $results = $this->model->getUserFields($request['email']); - return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]); + $account = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email'], $fails); + if (!empty($fails)) { + return $this->displayBadFields("display_register.html.twig", $fails); } - return $this->displayBadFields("display_register.html.twig", $fails); + + $session->setAccount($account); + + $target_url = $session->getInitialTarget(); + return HttpResponse::redirect($target_url ?? "/home"); } @@ -71,22 +76,26 @@ class AuthController { * @param mixed[] $request * @return HttpResponse */ - public function confirmLogin(array $request): HttpResponse { + public function confirmLogin(array $request, MutableSessionHandle $session): HttpResponse { $fails = []; $request = HttpRequest::from($request, $fails, [ "password" => [Validators::lenBetween(6, 256)], - "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"), Validators::lenBetween(5, 256)], ]); if (!empty($fails)) { return $this->displayBadFields("display_login.html.twig", $fails); } - $fails = $this->model->login($request['email'], $request['password']); - if (empty($fails)) { - $results = $this->model->getUserFields($request['email']); - return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]); + $account = $this->model->login($request['email'], $request['password'], $fails); + if (!empty($fails)) { + return $this->displayBadFields("display_login.html.twig", $fails); } - return $this->displayBadFields("display_login.html.twig", $fails); + + $session->setAccount($account); + + $target_url = $session->getInitialTarget(); + $session->setInitialTarget(null); + return HttpResponse::redirect($target_url ?? "/home"); } } diff --git a/src/Controller/Sub/EditorController.php b/src/Controller/Sub/EditorController.php index ce7a0a3..dff6e8a 100644 --- a/src/Controller/Sub/EditorController.php +++ b/src/Controller/Sub/EditorController.php @@ -3,6 +3,7 @@ namespace App\Controller\Sub; use App\Connexion; +use App\Controller\VisitorController; use App\Data\TacticInfo; use App\Gateway\TacticInfoGateway; use App\Http\HttpCodes; @@ -10,6 +11,9 @@ use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; use App\Model\TacticModel; +use App\Session\SessionHandle; +use App\Validation\ValidationFail; +use App\Validator\TacticValidator; class EditorController { private TacticModel $model; @@ -22,24 +26,28 @@ class EditorController { return ViewHttpResponse::react("views/Editor.tsx", ["name" => $tactic->getName(), "id" => $tactic->getId()]); } - public function createNew(): HttpResponse { - $tactic = $this->model->makeNewDefault(); + public function createNew(SessionHandle $session): HttpResponse { + $tactic = $this->model->makeNewDefault($session->getAccount()->getId()); return $this->openEditor($tactic); } /** * returns an editor view for a given tactic * @param int $id the targeted tactic identifier + * @param SessionHandle $session * @return HttpResponse */ - public function edit(int $id): HttpResponse { + public function edit(int $id, SessionHandle $session): HttpResponse { $tactic = $this->model->get($id); - if ($tactic == null) { - return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND); + $failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId()); + + if ($failure != null) { + return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); } return $this->openEditor($tactic); } + } diff --git a/src/Controller/Sub/VisualizerController.php b/src/Controller/Sub/VisualizerController.php index e3b5663..c7e5098 100644 --- a/src/Controller/Sub/VisualizerController.php +++ b/src/Controller/Sub/VisualizerController.php @@ -7,6 +7,9 @@ use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; use App\Model\TacticModel; +use App\Session\SessionHandle; +use App\Validation\ValidationFail; +use App\Validator\TacticValidator; class VisualizerController { private TacticModel $tacticModel; @@ -19,11 +22,13 @@ class VisualizerController { $this->tacticModel = $tacticModel; } - public function visualize(int $id): HttpResponse { + public function visualize(int $id, SessionHandle $session): HttpResponse { $tactic = $this->tacticModel->get($id); - if ($tactic == null) { - return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND); + $failure = TacticValidator::validateAccess($tactic, $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/Controller/UserController.php b/src/Controller/UserController.php index 58f0172..c2b9041 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -3,49 +3,38 @@ namespace App\Controller; use App\Connexion; -use App\Gateway\AuthGateway; use App\Gateway\TacticInfoGateway; use App\Gateway\TeamGateway; use App\Http\HttpResponse; use App\Http\ViewHttpResponse; -use App\Model\AuthModel; use App\Model\TacticModel; use App\Model\TeamModel; +use App\Session\SessionHandle; + +class UserController extends VisitorController { -class UserController { public function home(): HttpResponse { return ViewHttpResponse::twig("home.twig", []); } - public function register(): HttpResponse { - $model = new AuthModel(new AuthGateway(new Connexion(get_database()))); - if ($_SERVER['REQUEST_METHOD'] === 'GET') { - return (new Sub\AuthController($model))->displayRegister(); - } - return (new Sub\AuthController($model))->confirmRegister($_POST); - } - - public function login(): HttpResponse { - $model = new AuthModel(new AuthGateway(new Connexion(get_database()))); - if ($_SERVER['REQUEST_METHOD'] === 'GET') { - return (new Sub\AuthController($model))->displayLogin(); - } - return (new Sub\AuthController($model))->confirmLogin($_POST); + public function view(int $id, SessionHandle $session): HttpResponse { + $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); + return (new Sub\VisualizerController($model))->visualize($id, $session); } - public function open(int $id): HttpResponse { + public function edit(int $id, SessionHandle $session): HttpResponse { $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); - return (new Sub\VisualizerController($model))->visualize($id); + return (new Sub\EditorController($model))->edit($id, $session); } - public function edit(int $id): HttpResponse { + public function create(SessionHandle $session): HttpResponse { $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); - return (new Sub\EditorController($model))->edit($id); + return (new Sub\EditorController($model))->createNew($session); } - public function create(): HttpResponse { + public function open(int $id, SessionHandle $session): HttpResponse { $model = new TacticModel(new TacticInfoGateway(new Connexion(get_database()))); - return (new Sub\EditorController($model))->createNew(); + return (new Sub\VisualizerController($model))->visualize($id, $session); } public function createTeam(): HttpResponse { diff --git a/src/Controller/VisitorController.php b/src/Controller/VisitorController.php new file mode 100644 index 0000000..e74934a --- /dev/null +++ b/src/Controller/VisitorController.php @@ -0,0 +1,28 @@ +displayRegister(); + } + return (new Sub\AuthController($model))->confirmRegister($_POST, $session); + } + + final public function login(MutableSessionHandle $session): HttpResponse { + $model = new AuthModel(new AccountGateway(new Connexion(get_database()))); + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + return (new Sub\AuthController($model))->displayLogin(); + } + return (new Sub\AuthController($model))->confirmLogin($_POST, $session); + } + +} diff --git a/src/Data/Account.php b/src/Data/Account.php index 2a21bf1..0ed4339 100755 --- a/src/Data/Account.php +++ b/src/Data/Account.php @@ -4,8 +4,6 @@ namespace App\Data; use http\Exception\InvalidArgumentException; -const PHONE_NUMBER_REGEXP = "/^\\+[0-9]+$/"; - /** * Base class of a user account. * Contains the private information that we don't want @@ -16,90 +14,50 @@ class Account { * @var string $email account's mail address */ private string $email; - /** - * @var string account's phone number. - * its format is specified by the {@link PHONE_NUMBER_REGEXP} constant - * - */ - private string $phoneNumber; /** - * @var AccountUser account's public and shared information + * @var string string token */ - private AccountUser $user; + private string $token; /** - * @var Team[] account's teams + * @var string the account's username */ - private array $teams; + private string $name; /** - * @var int account's unique identifier + * @var int */ private int $id; /** * @param string $email - * @param string $phoneNumber - * @param AccountUser $user - * @param Team[] $teams + * @param string $name + * @param string $token * @param int $id */ - public function __construct(string $email, string $phoneNumber, AccountUser $user, array $teams, int $id) { + public function __construct(string $email, string $name, string $token, int $id) { $this->email = $email; - $this->phoneNumber = $phoneNumber; - $this->user = $user; - $this->teams = $teams; + $this->name = $name; + $this->token = $token; $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; + public function getId(): int { + return $this->id; } - /** - * @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 getEmail(): string { + return $this->email; } - public function getId(): int { - return $this->id; + public function getToken(): string { + return $this->token; } - /** - * @return Team[] - */ - public function getTeams(): array { - return $this->teams; + public function getName(): string { + return $this->name; } - public function getUser(): AccountUser { - return $this->user; - } } diff --git a/src/Data/TacticInfo.php b/src/Data/TacticInfo.php index eef7bb3..a2a576e 100644 --- a/src/Data/TacticInfo.php +++ b/src/Data/TacticInfo.php @@ -7,14 +7,18 @@ class TacticInfo implements \JsonSerializable { private string $name; private int $creation_date; + private int $ownerId; + /** * @param int $id * @param string $name * @param int $creation_date + * @param int $ownerId */ - public function __construct(int $id, string $name, int $creation_date) { + public function __construct(int $id, string $name, int $creation_date, int $ownerId) { $this->id = $id; $this->name = $name; + $this->ownerId = $ownerId; $this->creation_date = $creation_date; } @@ -26,6 +30,13 @@ class TacticInfo implements \JsonSerializable { return $this->name; } + /** + * @return int + */ + public function getOwnerId(): int { + return $this->ownerId; + } + public function getCreationTimestamp(): int { return $this->creation_date; } diff --git a/src/Gateway/AccountGateway.php b/src/Gateway/AccountGateway.php new file mode 100644 index 0000000..3a57abb --- /dev/null +++ b/src/Gateway/AccountGateway.php @@ -0,0 +1,74 @@ +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; + } + + + 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"]); + } + + +} diff --git a/src/Gateway/AuthGateway.php b/src/Gateway/AuthGateway.php deleted file mode 100644 index 5acc01c..0000000 --- a/src/Gateway/AuthGateway.php +++ /dev/null @@ -1,47 +0,0 @@ -con = $con; - } - - - public function mailExist(string $email): bool { - return $this->getUserFields($email) != null; - } - - - public function insertAccount(string $username, string $hash, string $email): void { - $this->con->exec("INSERT INTO AccountUser VALUES (:username,:hash,:email)", [':username' => [$username, PDO::PARAM_STR],':hash' => [$hash, PDO::PARAM_STR],':email' => [$email, PDO::PARAM_STR]]); - } - - public function getUserHash(string $email): string { - $results = $this->con->fetch("SELECT hash FROM AccountUser WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]); - return $results[0]['hash']; - } - - - /** - * @param string $email - * @return array|null - */ - public function getUserFields(string $email): ?array { - $results = $this->con->fetch("SELECT username,email FROM AccountUser WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]); - $firstRow = $results[0] ?? null; - return $firstRow; - } - - - - -} diff --git a/src/Gateway/FormResultGateway.php b/src/Gateway/FormResultGateway.php deleted file mode 100644 index 36178ad..0000000 --- a/src/Gateway/FormResultGateway.php +++ /dev/null @@ -1,35 +0,0 @@ -con = $con; - } - - - public function insert(string $username, string $description): void { - $this->con->exec( - "INSERT INTO FormEntries VALUES (:name, :description)", - [ - ":name" => [$username, PDO::PARAM_STR], - "description" => [$description, PDO::PARAM_STR], - ] - ); - } - - /** - * @return array - */ - public function listResults(): array { - return $this->con->fetch("SELECT * FROM FormEntries", []); - } -} diff --git a/src/Gateway/TacticInfoGateway.php b/src/Gateway/TacticInfoGateway.php index 3441c9a..3075f69 100644 --- a/src/Gateway/TacticInfoGateway.php +++ b/src/Gateway/TacticInfoGateway.php @@ -18,7 +18,7 @@ class TacticInfoGateway { public function get(int $id): ?TacticInfo { $res = $this->con->fetch( - "SELECT * FROM TacticInfo WHERE id = :id", + "SELECT * FROM Tactic WHERE id = :id", [":id" => [$id, PDO::PARAM_INT]] ); @@ -28,24 +28,27 @@ class TacticInfoGateway { $row = $res[0]; - return new TacticInfo($id, $row["name"], strtotime($row["creation_date"])); + return new TacticInfo($id, $row["name"], strtotime($row["creation_date"]), $row["owner"]); } - public function insert(string $name): TacticInfo { + public function insert(string $name, int $owner): TacticInfo { $this->con->exec( - "INSERT INTO TacticInfo(name) VALUES(:name)", - [":name" => [$name, PDO::PARAM_STR]] + "INSERT INTO Tactic(name, owner) VALUES(:name, :owner)", + [ + ":name" => [$name, PDO::PARAM_STR], + ":owner" => [$owner, PDO::PARAM_INT], + ] ); $row = $this->con->fetch( - "SELECT id, creation_date FROM TacticInfo WHERE :id = id", + "SELECT id, creation_date, owner FROM Tactic WHERE :id = id", [':id' => [$this->con->lastInsertId(), PDO::PARAM_INT]] )[0]; - return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"])); + return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"]), $row["owner"]); } public function updateName(int $id, string $name): void { $this->con->exec( - "UPDATE TacticInfo SET name = :name WHERE id = :id", + "UPDATE Tactic SET name = :name WHERE id = :id", [ ":name" => [$name, PDO::PARAM_STR], ":id" => [$id, PDO::PARAM_INT], diff --git a/src/Http/HttpCodes.php b/src/Http/HttpCodes.php index f9d550c..c2b01df 100644 --- a/src/Http/HttpCodes.php +++ b/src/Http/HttpCodes.php @@ -7,7 +7,11 @@ namespace App\Http; */ class HttpCodes { public const OK = 200; + public const FOUND = 302; public const BAD_REQUEST = 400; + + public const FORBIDDEN = 403; + public const NOT_FOUND = 404; } diff --git a/src/Http/HttpResponse.php b/src/Http/HttpResponse.php index 5d8c3bf..e5db3e8 100644 --- a/src/Http/HttpResponse.php +++ b/src/Http/HttpResponse.php @@ -3,21 +3,41 @@ namespace App\Http; class HttpResponse { + /** + * @var array + */ + private array $headers; private int $code; /** * @param int $code + * @param array $headers */ - public function __construct(int $code) { + public function __construct(int $code, array $headers) { $this->code = $code; + $this->headers = $headers; } public function getCode(): int { return $this->code; } + /** + * @return array + */ + public function getHeaders(): array { + return $this->headers; + } + public static function fromCode(int $code): HttpResponse { - return new HttpResponse($code); + return new HttpResponse($code, []); + } + + public static function redirect(string $url, int $code = HttpCodes::FOUND): HttpResponse { + if ($code < 300 || $code >= 400) { + throw new \InvalidArgumentException("given code is not a redirection http code"); + } + return new HttpResponse($code, ["Location" => $url]); } } diff --git a/src/Http/JsonHttpResponse.php b/src/Http/JsonHttpResponse.php index bbd3d80..a01ffd9 100644 --- a/src/Http/JsonHttpResponse.php +++ b/src/Http/JsonHttpResponse.php @@ -12,7 +12,7 @@ class JsonHttpResponse extends HttpResponse { * @param mixed $payload */ public function __construct($payload, int $code = HttpCodes::OK) { - parent::__construct($code); + parent::__construct($code, []); $this->payload = $payload; } diff --git a/src/Http/ViewHttpResponse.php b/src/Http/ViewHttpResponse.php index 2e517d7..9db80c5 100644 --- a/src/Http/ViewHttpResponse.php +++ b/src/Http/ViewHttpResponse.php @@ -26,7 +26,7 @@ class ViewHttpResponse extends HttpResponse { * @param array $arguments */ private function __construct(int $kind, string $file, array $arguments, int $code = HttpCodes::OK) { - parent::__construct($code); + parent::__construct($code, []); $this->kind = $kind; $this->file = $file; $this->arguments = $arguments; diff --git a/src/Model/AuthModel.php b/src/Model/AuthModel.php index 312cca8..1739ab5 100644 --- a/src/Model/AuthModel.php +++ b/src/Model/AuthModel.php @@ -2,16 +2,18 @@ namespace App\Model; -use App\Gateway\AuthGateway; +use App\Data\Account; +use App\Gateway\AccountGateway; use App\Validation\FieldValidationFail; use App\Validation\ValidationFail; class AuthModel { - private AuthGateway $gateway; + private AccountGateway $gateway; + /** - * @param AuthGateway $gateway + * @param AccountGateway $gateway */ - public function __construct(AuthGateway $gateway) { + public function __construct(AccountGateway $gateway) { $this->gateway = $gateway; } @@ -21,59 +23,54 @@ class AuthModel { * @param string $password * @param string $confirmPassword * @param string $email - * @return ValidationFail[] + * @param ValidationFail[] $failures + * @return Account|null the registered account or null if failures occurred */ - public function register(string $username, string $password, string $confirmPassword, string $email): array { - $errors = []; + public function register(string $username, string $password, string $confirmPassword, string $email, array &$failures): ?Account { if ($password != $confirmPassword) { - $errors[] = new FieldValidationFail("confirmpassword", "password and password confirmation are not equals"); + $failures[] = new FieldValidationFail("confirmpassword", "password and password confirmation are not equals"); } - if ($this->gateway->mailExist($email)) { - $errors[] = new FieldValidationFail("email", "email already exist"); + if ($this->gateway->exists($email)) { + $failures[] = new FieldValidationFail("email", "email already exist"); } - if(empty($errors)) { - $hash = password_hash($password, PASSWORD_DEFAULT); - $this->gateway->insertAccount($username, $hash, $email); + if (!empty($failures)) { + return null; } - return $errors; - } + $hash = password_hash($password, PASSWORD_DEFAULT); - /** - * @param string $email - * @return array|null - */ - public function getUserFields(string $email): ?array { - return $this->gateway->getUserFields($email); + $token = $this->generateToken(); + $accountId = $this->gateway->insertAccount($username, $email, $token, $hash); + return new Account($email, $username, $token, $accountId); } + private function generateToken(): string { + return base64_encode(random_bytes(64)); + } /** * @param string $email * @param string $password - * @return ValidationFail[] $errors + * @param ValidationFail[] $failures + * @return Account|null the authenticated account or null if failures occurred */ - public function login(string $email, string $password): array { - $errors = []; - - if (!$this->gateway->mailExist($email)) { - $errors[] = new FieldValidationFail("email", "email doesnt exists"); - return $errors; + public function login(string $email, string $password, array &$failures): ?Account { + if (!$this->gateway->exists($email)) { + $failures[] = new FieldValidationFail("email", "email doesnt exists"); + return null; } - $hash = $this->gateway->getUserHash($email); + $hash = $this->gateway->getHash($email); if (!password_verify($password, $hash)) { - $errors[] = new FieldValidationFail("password", "invalid password"); + $failures[] = new FieldValidationFail("password", "invalid password"); + return null; } - return $errors; + return $this->gateway->getAccountFromMail($email); } - - - } diff --git a/src/Model/TacticModel.php b/src/Model/TacticModel.php index cabcdec..8111963 100644 --- a/src/Model/TacticModel.php +++ b/src/Model/TacticModel.php @@ -2,8 +2,10 @@ namespace App\Model; +use App\Data\Account; use App\Data\TacticInfo; use App\Gateway\TacticInfoGateway; +use App\Validation\ValidationFail; class TacticModel { public const TACTIC_DEFAULT_NAME = "Nouvelle tactique"; @@ -18,12 +20,12 @@ class TacticModel { $this->tactics = $tactics; } - public function makeNew(string $name): TacticInfo { - return $this->tactics->insert($name); + public function makeNew(string $name, int $ownerId): TacticInfo { + return $this->tactics->insert($name, $ownerId); } - public function makeNewDefault(): ?TacticInfo { - return $this->tactics->insert(self::TACTIC_DEFAULT_NAME); + public function makeNewDefault(int $ownerId): ?TacticInfo { + return $this->tactics->insert(self::TACTIC_DEFAULT_NAME, $ownerId); } /** @@ -39,15 +41,22 @@ class TacticModel { * Update the name of a tactic * @param int $id the tactic identifier * @param string $name the new name to set - * @return bool true if the update was done successfully + * @return ValidationFail[] failures, if any */ - public function updateName(int $id, string $name): bool { - if ($this->tactics->get($id) == null) { - return false; + public function updateName(int $id, string $name, int $authId): array { + + $tactic = $this->tactics->get($id); + + if ($tactic == null) { + return [ValidationFail::notFound("Could not find tactic")]; + } + + if ($tactic->getOwnerId() != $authId) { + return [ValidationFail::unauthorized()]; } $this->tactics->updateName($id, $name); - return true; + return []; } } diff --git a/src/Session/MutableSessionHandle.php b/src/Session/MutableSessionHandle.php new file mode 100644 index 0000000..1459819 --- /dev/null +++ b/src/Session/MutableSessionHandle.php @@ -0,0 +1,20 @@ +getOwnerId() != $ownerId) { + return ValidationFail::unauthorized("Vous ne pouvez pas accéder à cette tactique."); + } + + return null; + } + +} diff --git a/src/Views/error.html.twig b/src/Views/error.html.twig index 1d1db7d..e30c2fa 100644 --- a/src/Views/error.html.twig +++ b/src/Views/error.html.twig @@ -51,7 +51,7 @@ {% endfor %} - + \ No newline at end of file