Add Session handling #19

Merged
maxime.batista merged 9 commits from session into salva 1 year ago

@ -2,12 +2,10 @@
object Account {
<u>id
name
age
email
phoneNumber
passwordHash
profilePicture
username
token
hash
}
object Team {
@ -26,7 +24,7 @@ object TacticFolder {
object Tactic {
<u>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

@ -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

@ -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

@ -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) {

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

@ -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
);

@ -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;
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) {

@ -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<string, mixed> $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<int, mixed> $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) {

@ -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");
}
}

@ -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);
}
}

@ -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()]);

@ -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 {

@ -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;
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;
}
}

@ -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;
}

@ -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;
}
}

@ -1,35 +0,0 @@
<?php
namespace App\Gateway;
use PDO;
use App\Connexion;
/**
* A sample gateway, that stores the sample form's result.
*/
class FormResultGateway {
private Connexion $con;
public function __construct(Connexion $con) {
$this->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<string, mixed>
*/
public function listResults(): array {
return $this->con->fetch("SELECT * FROM FormEntries", []);
}
}

@ -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],

@ -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;
}

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

@ -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;
}

@ -26,7 +26,7 @@ class ViewHttpResponse extends HttpResponse {
* @param array<string, mixed> $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;

@ -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<string,string>|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);
}
}

@ -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 [];
}
}

@ -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 {
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);
}
}

@ -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;
}
}

@ -51,7 +51,7 @@
{% 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>
</html>
Loading…
Cancel
Save