integrate session in API, use tokens

pull/19/head
Override-6 1 year ago committed by maxime.batista
parent 982acf5e09
commit 39329c9325

@ -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,134 @@ require "../../sql/database.php";
require "../utils.php";
use App\Connexion;
use App\Controller\Api\APIAuthController;
use App\Controller\Api\APITacticController;
use App\Data\Account;
use App\Gateway\AccountGateway;
use App\Gateway\TacticInfoGateway;
use App\Http\HttpResponse;
use App\Http\JsonHttpResponse;
use App\Http\ViewHttpResponse;
use App\Model\AuthModel;
use App\Model\TacticModel;
use App\Session\SessionHandle;
use App\Validation\ValidationFail;
$con = new Connexion(get_database());
function getTacticController(): APITacticController {
return new APITacticController(new TacticModel(new TacticInfoGateway(new Connexion(get_database()))));
}
function getAuthController(): APIAuthController {
return new APIAuthController(new AuthModel(new AccountGateway(new Connexion(get_database()))));
}
class Action {
/**
* @var callable(mixed[]): HttpResponse $action action to call
*/
private $action;
private bool $isAuthRequired;
/**
* @param callable(mixed[]): HttpResponse $action
*/
private function __construct(callable $action, bool $isAuthRequired) {
$this->action = $action;
$this->isAuthRequired = $isAuthRequired;
}
public function isAuthRequired(): bool {
return $this->isAuthRequired;
}
/**
* @param mixed[] $params
* @param ?Account $account
* @return HttpResponse
*/
public function run(array $params, ?Account $account): HttpResponse {
$params = array_values($params);
if ($this->isAuthRequired) {
if ($account == null) {
throw new Exception("action requires authorization.");
}
$params[] = $account;
}
return call_user_func_array($this->action, $params);
}
/**
* @param callable(mixed[]): HttpResponse $action
* @return Action an action that does not require to have an authorization.
*/
public static function noAuth(callable $action): Action {
return new Action($action, false);
}
/**
* @param callable(mixed[]): HttpResponse $action
* @return Action an action that does require to have an authorization.
*/
public static function auth(callable $action): Action {
return new Action($action, true);
}
}
/**
* @param mixed[] $match
* @return HttpResponse
* @throws Exception
*/
function handleMatch(array $match): HttpResponse {
if (!$match) {
return new JsonHttpResponse([ValidationFail::notFound("not found")]);
}
$action = $match['target'];
if (!$action instanceof Action) {
throw new Exception("routed action is not an Action object.");
}
$auth = null;
if ($action->isAuthRequired()) {
$auth = tryGetAuthAccount();
if ($auth == null) {
return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header")]);
}
}
return $action->run($match['params'], $auth);
}
function tryGetAuthAccount(): ?Account {
$headers = getallheaders();
// If no authorization header is set, try fallback to php session.
if (!isset($headers['Authorization'])) {
$session = SessionHandle::init();
return $session->getAccount();
}
$token = $headers['Authorization'];
$gateway = new AccountGateway(new Connexion(get_database()));
return $gateway->getAccountFromToken($token);
}
$router = new AltoRouter();
$router->setBasePath(get_public_path() . "/api");
$tacticEndpoint = new APITacticController(new TacticModel(new TacticInfoGateway($con)));
$router->map("POST", "/tactic/[i:id]/edit/name", fn(int $id) => $tacticEndpoint->updateName($id));
$router->map("GET", "/tactic/[i:id]", fn(int $id) => $tacticEndpoint->getTacticInfo($id));
$router->map("POST", "/tactic/new", fn() => $tacticEndpoint->newTactic());
$router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc)));
$router->map("GET", "/tactic/[i:id]", Action::auth(fn(int $id, Account $acc) => getTacticController()->getTacticInfo($id, $acc)));
$router->map("POST", "/tactic/new", Action::auth(fn(Account $acc) => getTacticController()->newTactic($acc)));
$router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize()));
$match = $router->match();
if ($match == null) {
echo "404 not found";
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
exit(1);
}
$response = call_user_func_array($match['target'], $match['params']);
$response = handleMatch($match);
http_response_code($response->getCode());
if ($response instanceof JsonHttpResponse) {

@ -8,9 +8,9 @@ require "utils.php";
require "../src/react-display.php";
use App\Controller\FrontController;
use App\Session\PhpSessionHandle;
use App\Session\SessionHandle;
$basePath = get_public_path();
$frontController = new FrontController($basePath);
$frontController->run(PhpSessionHandle::init());
$frontController->run(SessionHandle::init());

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

@ -42,7 +42,7 @@ class FrontController {
$this->handleMatch($match, $session);
} else {
$this->displayViewByKind(ViewHttpResponse::twig("error.html.twig", [
'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")]
'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")],
], HttpCodes::NOT_FOUND));
}
}
@ -108,7 +108,7 @@ class FrontController {
return call_user_func_array([$controller, $action], $params);
} else {
return ViewHttpResponse::twig("error.html.twig", [
'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")]
'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")],
], HttpCodes::NOT_FOUND);
}
}

@ -2,7 +2,6 @@
namespace App\Controller\Sub;
use App\Gateway\AccountGateway;
use App\Http\HttpRequest;
use App\Http\HttpResponse;
use App\Http\ViewHttpResponse;

@ -40,7 +40,7 @@ class EditorController {
public function edit(int $id, SessionHandle $session): HttpResponse {
$tactic = $this->model->get($id);
$failure = TacticValidator::validateAccess($tactic, $session->getAccount()->getId());
$failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId());
if ($failure != null) {
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);

@ -25,7 +25,7 @@ class VisualizerController {
public function visualize(int $id, SessionHandle $session): HttpResponse {
$tactic = $this->tacticModel->get($id);
$failure = TacticValidator::validateAccess($tactic, $session->getAccount()->getId());
$failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId());
if ($failure != null) {
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);

@ -9,9 +9,7 @@ use App\Model\AuthModel;
use App\Session\MutableSessionHandle;
class VisitorController {
public final function register(MutableSessionHandle $session): HttpResponse {
final public function register(MutableSessionHandle $session): HttpResponse {
$model = new AuthModel(new AccountGateway(new Connexion(get_database())));
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
return (new Sub\AuthController($model))->displayRegister();
@ -19,7 +17,7 @@ class VisitorController {
return (new Sub\AuthController($model))->confirmRegister($_POST, $session);
}
public final function login(MutableSessionHandle $session): HttpResponse {
final public function login(MutableSessionHandle $session): HttpResponse {
$model = new AuthModel(new AccountGateway(new Connexion(get_database())));
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
return (new Sub\AuthController($model))->displayLogin();
@ -27,4 +25,4 @@ class VisitorController {
return (new Sub\AuthController($model))->confirmLogin($_POST, $session);
}
}
}

@ -17,39 +17,56 @@ class AccountGateway {
}
public function exists(string $email): bool {
return $this->getAccount($email) != null;
}
public function insertAccount(string $name, string $email, string $token, string $hash): int {
$this->con->exec("INSERT INTO Account(username, hash, email, token) VALUES (:username,:hash,:email,:token)", [
':username' => [$name, PDO::PARAM_STR],
':hash' => [$hash, PDO::PARAM_STR],
':email' => [$email, PDO::PARAM_STR],
':token' => [$token, PDO::PARAM_STR]
':token' => [$token, PDO::PARAM_STR],
]);
return intval($this->con->lastInsertId());
}
public function getHash(string $email): string {
$results = $this->con->fetch("SELECT hash FROM Account WHERE email = :email", [
':email' => [$email, PDO::PARAM_STR]
]);
return $results[0]['hash'];
/**
* @param string $email
* @return array<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 getAccount(string $email): ?Account {
$results = $this->con->fetch("SELECT * FROM Account WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]);
if (empty($results))
public function getAccountFromMail(string $email): ?Account {
$acc = $this->getRowsFromMail($email);
if (empty($acc)) {
return null;
}
return new Account($email, $acc["username"], $acc["token"], $acc["id"]);
}
public function getAccountFromToken(string $token): ?Account {
$acc = $this->con->fetch("SELECT * FROM Account WHERE token = :token", [':token' => [$token, PDO::PARAM_STR]])[0] ?? null;
if (empty($acc)) {
return null;
}
$acc = $results[0];
return new Account($acc["email"], $acc["username"], $acc["token"], $acc["id"]);
}

@ -36,7 +36,7 @@ class TacticInfoGateway {
"INSERT INTO TacticInfo(name, owner) VALUES(:name, :owner)",
[
":name" => [$name, PDO::PARAM_STR],
":owner" => [$owner, PDO::PARAM_INT]
":owner" => [$owner, PDO::PARAM_INT],
]
);
$row = $this->con->fetch(

@ -3,7 +3,6 @@
namespace App\Http;
class HttpResponse {
/**
* @var array<string, string>
*/

@ -69,7 +69,7 @@ class AuthModel {
return null;
}
return $this->gateway->getAccount($email);
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";
@ -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 [];
}
}

@ -5,6 +5,5 @@ namespace App\Session;
use App\Data\Account;
interface MutableSessionHandle extends SessionHandle {
public function setAccount(Account $account): void;
}
}

@ -5,8 +5,7 @@ namespace App\Session;
use App\Data\Account;
class PhpSessionHandle implements MutableSessionHandle {
public static function init(): PhpSessionHandle {
public static function init(): SessionHandle {
if (session_status() !== PHP_SESSION_NONE) {
throw new \Exception("A php session is already started !");
}
@ -21,4 +20,4 @@ class PhpSessionHandle implements MutableSessionHandle {
public function setAccount(Account $account): void {
$_SESSION["account"] = $account;
}
}
}

@ -5,7 +5,5 @@ namespace App\Session;
use App\Data\Account;
interface SessionHandle {
public function getAccount(): ?Account;
}
}

@ -37,4 +37,8 @@ class ValidationFail implements JsonSerializable {
return new ValidationFail("Not found", $message);
}
public static function unauthorized(string $message = "Unauthorized"): ValidationFail {
return new ValidationFail("Unauthorized", $message);
}
}

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

Loading…
Cancel
Save