Merge Wednesday Rush and remove Salva's imposed conception aberrations #22
Merged
maxime.batista
merged 48 commits from salva
into master
1 year ago
@ -0,0 +1,28 @@
|
|||||||
|
@startuml
|
||||||
|
|
||||||
|
class AuthController {
|
||||||
|
+ displayRegister() : HttpResponse
|
||||||
|
+ displayBadFields(viewName : string, fails : array) : HttpResponse
|
||||||
|
+ confirmRegister(request : array) : HttpResponse
|
||||||
|
+ displayLogin() : HttpResponse
|
||||||
|
+ confirmLogin() : HttpResponse
|
||||||
|
}
|
||||||
|
AuthController --> "- model" AuthModel
|
||||||
|
|
||||||
|
class AuthModel {
|
||||||
|
+ register(username : string, password : string, confirmPassword : string, email : string): array
|
||||||
|
+ getAccount(email : string):array
|
||||||
|
+ login(email : string, password : string)
|
||||||
|
}
|
||||||
|
AuthModel --> "- gateway" AuthGateway
|
||||||
|
|
||||||
|
class AuthGateway {
|
||||||
|
-con : Connection
|
||||||
|
|
||||||
|
+ mailExists(email : string) : bool
|
||||||
|
+ insertAccount(username : string, hash : string, email : string)
|
||||||
|
+ getUserHash(email : string):string
|
||||||
|
+ getAccount (email : string): array
|
||||||
|
}
|
||||||
|
|
||||||
|
@enduml
|
After Width: | Height: | Size: 507 B |
After Width: | Height: | Size: 732 B |
@ -1,95 +1,109 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
require "../vendor/autoload.php";
|
require "../vendor/autoload.php";
|
||||||
require "../config.php";
|
require "../config.php";
|
||||||
require "../sql/database.php";
|
require "../sql/database.php";
|
||||||
require "utils.php";
|
require "../src/utils.php";
|
||||||
|
require "../src/App/react-display.php";
|
||||||
use App\Connexion;
|
|
||||||
use App\Controller\EditorController;
|
use IQBall\App\App;
|
||||||
use App\Controller\SampleFormController;
|
use IQBall\App\Controller\AuthController;
|
||||||
use App\Gateway\FormResultGateway;
|
use IQBall\App\Controller\EditorController;
|
||||||
use App\Gateway\TacticInfoGateway;
|
use IQBall\App\Controller\TeamController;
|
||||||
use App\Http\JsonHttpResponse;
|
use IQBall\App\Controller\UserController;
|
||||||
use App\Http\ViewHttpResponse;
|
use IQBall\App\Controller\VisualizerController;
|
||||||
use App\Model\TacticModel;
|
use IQBall\App\ViewHttpResponse;
|
||||||
use Twig\Loader\FilesystemLoader;
|
use IQBall\Core\Action;
|
||||||
use App\Gateway\AuthGateway;
|
use IQBall\Core\Connection;
|
||||||
use App\Controller\AuthController;
|
use IQBall\Core\Gateway\AccountGateway;
|
||||||
use App\Validation\ValidationFail;
|
use IQBall\Core\Gateway\MemberGateway;
|
||||||
use App\Controller\ErrorController;
|
use IQBall\Core\Gateway\TacticInfoGateway;
|
||||||
use App\Controller\VisualizerController;
|
use IQBall\Core\Gateway\TeamGateway;
|
||||||
|
use IQBall\Core\Http\HttpCodes;
|
||||||
$loader = new FilesystemLoader('../src/Views/');
|
use IQBall\Core\Http\HttpResponse;
|
||||||
$twig = new \Twig\Environment($loader);
|
use IQBall\Core\Model\AuthModel;
|
||||||
|
use IQBall\Core\Model\TacticModel;
|
||||||
|
use IQBall\Core\Model\TeamModel;
|
||||||
|
use IQBall\Core\Session\MutableSessionHandle;
|
||||||
|
use IQBall\Core\Session\PhpSessionHandle;
|
||||||
|
use IQBall\Core\Session\SessionHandle;
|
||||||
|
use IQBall\Core\Validation\ValidationFail;
|
||||||
|
|
||||||
|
function getConnection(): Connection {
|
||||||
|
return new Connection(get_database());
|
||||||
|
}
|
||||||
|
|
||||||
$basePath = get_public_path();
|
function getUserController(): UserController {
|
||||||
$con = new Connexion(get_database());
|
return new UserController(new TacticModel(new TacticInfoGateway(getConnection())));
|
||||||
|
|
||||||
// routes initialization
|
|
||||||
$router = new AltoRouter();
|
|
||||||
$router->setBasePath($basePath);
|
|
||||||
|
|
||||||
$sampleFormController = new SampleFormController(new FormResultGateway($con));
|
|
||||||
$authGateway = new AuthGateway($con);
|
|
||||||
$authController = new \App\Controller\AuthController(new \App\Model\AuthModel($authGateway));
|
|
||||||
$editorController = new EditorController(new TacticModel(new TacticInfoGateway($con)));
|
|
||||||
$visualizerController = new VisualizerController(new TacticModel(new TacticInfoGateway($con)));
|
|
||||||
|
|
||||||
|
|
||||||
$router->map("GET", "/", fn() => $sampleFormController->displayFormReact());
|
|
||||||
$router->map("POST", "/submit", fn() => $sampleFormController->submitFormReact($_POST));
|
|
||||||
$router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig());
|
|
||||||
$router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST));
|
|
||||||
$router->map("GET", "/register", fn() => $authController->displayRegister());
|
|
||||||
$router->map("POST", "/register", fn() => $authController->confirmRegister($_POST));
|
|
||||||
$router->map("GET", "/login", fn() => $authController->displayLogin());
|
|
||||||
$router->map("POST", "/login", fn() => $authController->confirmLogin($_POST));
|
|
||||||
$router->map("GET", "/tactic/new", fn() => $editorController->makeNew());
|
|
||||||
$router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->openEditorFor($id));
|
|
||||||
$router->map("GET", "/tactic/[i:id]", fn(int $id) => $visualizerController->openVisualizer($id));
|
|
||||||
|
|
||||||
$teamController = new \App\Controller\TeamController(new \App\Model\TeamModel(new \App\Gateway\TeamGateway($con)));
|
|
||||||
$router->map("GET", "/team/new", fn() => $teamController->displaySubmitTeam());
|
|
||||||
$router->map("POST", "/team/new", fn() => $teamController->SubmitTeam($_POST));
|
|
||||||
|
|
||||||
$router->map("GET", "/team/list", fn() => $teamController->displayListTeamByName());
|
|
||||||
$router->map("POST", "/team/list", fn() => $teamController->ListTeamByName($_POST));
|
|
||||||
|
|
||||||
$router->map("GET", "/team/[i:id]", fn(int $id) => $teamController->getTeam($id));
|
|
||||||
|
|
||||||
$match = $router->match();
|
|
||||||
|
|
||||||
if ($match == null) {
|
|
||||||
http_response_code(404);
|
|
||||||
ErrorController::displayFailures([ValidationFail::notFound("Cette page n'existe pas")], $twig);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = call_user_func_array($match['target'], $match['params']);
|
function getVisualizerController(): VisualizerController {
|
||||||
|
return new VisualizerController(new TacticModel(new TacticInfoGateway(getConnection())));
|
||||||
http_response_code($response->getCode());
|
}
|
||||||
|
|
||||||
if ($response instanceof ViewHttpResponse) {
|
function getEditorController(): EditorController {
|
||||||
$file = $response->getFile();
|
return new EditorController(new TacticModel(new TacticInfoGateway(getConnection())));
|
||||||
$args = $response->getArguments();
|
}
|
||||||
|
|
||||||
switch ($response->getViewKind()) {
|
function getTeamController(): TeamController {
|
||||||
case ViewHttpResponse::REACT_VIEW:
|
$con = getConnection();
|
||||||
send_react_front($file, $args);
|
return new TeamController(new TeamModel(new TeamGateway($con), new MemberGateway($con), new AccountGateway($con)));
|
||||||
break;
|
}
|
||||||
case ViewHttpResponse::TWIG_VIEW:
|
|
||||||
try {
|
function getAuthController(): AuthController {
|
||||||
$twig->display($file, $args);
|
return new AuthController(new AuthModel(new AccountGateway(getConnection())));
|
||||||
} catch (\Twig\Error\RuntimeError|\Twig\Error\SyntaxError $e) {
|
}
|
||||||
http_response_code(500);
|
|
||||||
echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s");
|
function getRoutes(): AltoRouter {
|
||||||
throw $e;
|
global $basePath;
|
||||||
}
|
|
||||||
break;
|
$ar = new AltoRouter();
|
||||||
|
$ar->setBasePath($basePath);
|
||||||
|
|
||||||
|
//authentication
|
||||||
|
$ar->map("GET", "/login", Action::noAuth(fn() => getAuthController()->displayLogin()));
|
||||||
|
$ar->map("GET", "/register", Action::noAuth(fn() => getAuthController()->displayRegister()));
|
||||||
|
$ar->map("POST", "/login", Action::noAuth(fn(SessionHandle $s) => getAuthController()->login($_POST, $s)));
|
||||||
|
$ar->map("POST", "/register", Action::noAuth(fn(SessionHandle $s) => getAuthController()->register($_POST, $s)));
|
||||||
|
|
||||||
|
//user-related
|
||||||
|
$ar->map("GET", "/home", Action::auth(fn(SessionHandle $s) => getUserController()->home($s)));
|
||||||
|
$ar->map("GET", "/", Action::auth(fn(SessionHandle $s) => getUserController()->home($s)));
|
||||||
|
$ar->map("GET", "/settings", Action::auth(fn(SessionHandle $s) => getUserController()->settings($s)));
|
||||||
|
|
||||||
|
//tactic-related
|
||||||
|
$ar->map("GET", "/tactic/[i:id]/view", Action::auth(fn(int $id, SessionHandle $s) => getVisualizerController()->openVisualizer($id, $s)));
|
||||||
|
$ar->map("GET", "/tactic/[i:id]/edit", Action::auth(fn(int $id, SessionHandle $s) => getEditorController()->openEditor($id, $s)));
|
||||||
|
$ar->map("GET", "/tactic/new", Action::auth(fn(SessionHandle $s) => getEditorController()->createNew($s)));
|
||||||
|
|
||||||
|
//team-related
|
||||||
|
$ar->map("GET", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->displayCreateTeam($s)));
|
||||||
|
$ar->map("POST", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->submitTeam($_POST, $s)));
|
||||||
|
$ar->map("GET", "/team/search", Action::auth(fn(SessionHandle $s) => getTeamController()->displayListTeamByName($s)));
|
||||||
|
$ar->map("POST", "/team/search", Action::auth(fn(SessionHandle $s) => getTeamController()->listTeamByName($_POST, $s)));
|
||||||
|
$ar->map("GET", "/team/[i:id]", Action::auth(fn(int $id, SessionHandle $s) => getTeamController()->displayTeam($id, $s)));
|
||||||
|
$ar->map("GET", "/team/members/add", Action::auth(fn(SessionHandle $s) => getTeamController()->displayAddMember($s)));
|
||||||
|
$ar->map("POST", "/team/members/add", Action::auth(fn(SessionHandle $s) => getTeamController()->addMember($_POST, $s)));
|
||||||
|
$ar->map("GET", "/team/members/remove", Action::auth(fn(SessionHandle $s) => getTeamController()->displayDeleteMember($s)));
|
||||||
|
$ar->map("POST", "/team/members/remove", Action::auth(fn(SessionHandle $s) => getTeamController()->deleteMember($_POST, $s)));
|
||||||
|
|
||||||
|
return $ar;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runMatch($match, MutableSessionHandle $session): HttpResponse {
|
||||||
|
global $basePath;
|
||||||
|
if (!$match) {
|
||||||
|
return ViewHttpResponse::twig("error.html.twig", [
|
||||||
|
'failures' => [ValidationFail::notFound("Could not find page {$_SERVER['REQUEST_URI']}.")],
|
||||||
|
], HttpCodes::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
} elseif ($response instanceof JsonHttpResponse) {
|
return App::runAction($basePath . '/login', $match['target'], $match['params'], $session);
|
||||||
header('Content-type: application/json');
|
|
||||||
echo $response->getJson();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//this is a global variable
|
||||||
|
$basePath = get_public_path();
|
||||||
|
|
||||||
|
App::render(runMatch(getRoutes()->match(), PhpSessionHandle::init()), "../src/App/Views/");
|
||||||
|
@ -1,40 +1,44 @@
|
|||||||
-- drop tables here
|
-- drop tables here
|
||||||
DROP TABLE IF EXISTS FormEntries;
|
DROP TABLE IF EXISTS Account;
|
||||||
DROP TABLE IF EXISTS AccountUser;
|
DROP TABLE IF EXISTS Tactic;
|
||||||
DROP TABLE IF EXISTS TacticInfo;
|
|
||||||
DROP TABLE IF EXISTS Team;
|
DROP TABLE IF EXISTS Team;
|
||||||
DROP TABLE IF EXISTS User;
|
DROP TABLE IF EXISTS User;
|
||||||
DROP TABLE IF EXISTS Member;
|
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 Tactic
|
||||||
CREATE TABLE AccountUser(
|
(
|
||||||
username varchar,
|
id integer PRIMARY KEY AUTOINCREMENT,
|
||||||
hash varchar,
|
name varchar NOT NULL,
|
||||||
email varchar unique
|
creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
owner integer NOT NULL,
|
||||||
|
FOREIGN KEY (owner) REFERENCES Account
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE Team(
|
CREATE TABLE FormEntries(name varchar, description varchar);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE Team
|
||||||
|
(
|
||||||
id integer PRIMARY KEY AUTOINCREMENT,
|
id integer PRIMARY KEY AUTOINCREMENT,
|
||||||
name varchar,
|
name varchar,
|
||||||
picture varchar,
|
picture varchar,
|
||||||
mainColor varchar,
|
main_color varchar,
|
||||||
secondColor varchar
|
second_color varchar
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE User(
|
|
||||||
id integer PRIMARY KEY AUTOINCREMENT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE Member(
|
CREATE TABLE Member(
|
||||||
idTeam integer,
|
id_team integer,
|
||||||
idMember integer,
|
id_user integer,
|
||||||
role char(1) CHECK (role IN ('C','P')),
|
role char(1) CHECK (role IN ('Coach', 'Player')),
|
||||||
FOREIGN KEY (idTeam) REFERENCES Team(id),
|
FOREIGN KEY (id_team) REFERENCES Team (id),
|
||||||
FOREIGN KEY (idMember) REFERENCES User(id)
|
FOREIGN KEY (id_user) REFERENCES User (id)
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE TacticInfo(
|
|
||||||
id integer PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name varchar,
|
|
||||||
creation_date timestamp DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\Api;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use IQBall\Core\Action;
|
||||||
|
use IQBall\Core\Connection;
|
||||||
|
use IQBall\Core\Data\Account;
|
||||||
|
use IQBall\Core\Gateway\AccountGateway;
|
||||||
|
use IQBall\Core\Http\HttpResponse;
|
||||||
|
use IQBall\Core\Http\JsonHttpResponse;
|
||||||
|
use IQBall\Core\Session\PhpSessionHandle;
|
||||||
|
use IQBall\Core\Validation\ValidationFail;
|
||||||
|
|
||||||
|
class API {
|
||||||
|
public static function render(HttpResponse $response): void {
|
||||||
|
http_response_code($response->getCode());
|
||||||
|
|
||||||
|
foreach ($response->getHeaders() as $header => $value) {
|
||||||
|
header("$header: $value");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response instanceof JsonHttpResponse) {
|
||||||
|
header('Content-type: application/json');
|
||||||
|
echo $response->getJson();
|
||||||
|
} else {
|
||||||
|
throw new Exception("API returned a non-json response.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed[] $match
|
||||||
|
* @return HttpResponse
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public static function handleMatch(array $match): HttpResponse {
|
||||||
|
if (!$match) {
|
||||||
|
return new JsonHttpResponse([ValidationFail::notFound("not found")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $match['target'];
|
||||||
|
if (!$action instanceof Action) {
|
||||||
|
throw new Exception("routed action is not an AppAction object.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$auth = null;
|
||||||
|
|
||||||
|
if ($action->isAuthRequired()) {
|
||||||
|
$auth = self::tryGetAuthorization();
|
||||||
|
if ($auth == null) {
|
||||||
|
return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $action->run($match['params'], $auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function tryGetAuthorization(): ?Account {
|
||||||
|
$headers = getallheaders();
|
||||||
|
|
||||||
|
// If no authorization header is set, try fallback to php session.
|
||||||
|
if (!isset($headers['Authorization'])) {
|
||||||
|
$session = PhpSessionHandle::init();
|
||||||
|
return $session->getAccount();
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $headers['Authorization'];
|
||||||
|
$gateway = new AccountGateway(new Connection(get_database()));
|
||||||
|
return $gateway->getAccountFromToken($token);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||||||||
|
<?php
|
||||||||||||
|
|
||||||||||||
|
namespace IQBall\Api\Controller;
|
||||||||||||
|
|
||||||||||||
|
use IQBall\Core\Http\HttpCodes;
|
||||||||||||
|
use IQBall\Core\Route\Control;
|
||||||||||||
|
use IQBall\Core\Http\HttpRequest;
|
||||||||||||
|
use IQBall\Core\Http\HttpResponse;
|
||||||||||||
|
use IQBall\Core\Http\JsonHttpResponse;
|
||||||||||||
|
use IQBall\Core\Model\AuthModel;
|
||||||||||||
|
use IQBall\Core\Validation\Validators;
|
||||||||||||
|
|
||||||||||||
|
class APIAuthController {
|
||||||||||||
|
private AuthModel $model;
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param AuthModel $model
|
||||||||||||
|
*/
|
||||||||||||
|
public function __construct(AuthModel $model) {
|
||||||||||||
|
$this->model = $model;
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
maxime.batista marked this conversation as resolved
|
|||||||||||||
|
/**
|
||||||||||||
|
* From given email address and password, authenticate the user and respond with its authorization token.
|
||||||||||||
|
* @return HttpResponse
|
||||||||||||
|
*/
|
||||||||||||
|
public function authorize(): HttpResponse {
|
||||||||||||
|
return Control::runChecked([
|
||||||||||||
|
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
|
||||||||||||
|
"password" => [Validators::lenBetween(6, 256)],
|
||||||||||||
|
], function (HttpRequest $req) {
|
||||||||||||
|
$failures = [];
|
||||||||||||
|
$account = $this->model->login($req["email"], $req["password"], $failures);
|
||||||||||||
|
|
||||||||||||
|
if (!empty($failures)) {
|
||||||||||||
|
return new JsonHttpResponse($failures, HttpCodes::UNAUTHORIZED);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return new JsonHttpResponse(["authorization" => $account->getToken()]);
|
||||||||||||
|
}, true);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\Api\Controller;
|
||||||
|
|
||||||
|
use IQBall\Core\Route\Control;
|
||||||
|
use IQBall\Core\Data\Account;
|
||||||
|
use IQBall\Core\Http\HttpCodes;
|
||||||
|
use IQBall\Core\Http\HttpRequest;
|
||||||
|
use IQBall\Core\Http\HttpResponse;
|
||||||
|
use IQBall\Core\Http\JsonHttpResponse;
|
||||||
|
use IQBall\Core\Model\TacticModel;
|
||||||
|
use IQBall\Core\Validation\Validators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API endpoint related to tactics
|
||||||
|
*/
|
||||||
|
class APITacticController {
|
||||||
|
private TacticModel $model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TacticModel $model
|
||||||
|
*/
|
||||||
|
public function __construct(TacticModel $model) {
|
||||||
|
$this->model = $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update name of tactic, specified by tactic identifier, given in url.
|
||||||
|
* @param int $tactic_id
|
||||||
|
* @param Account $account
|
||||||
|
* @return HttpResponse
|
||||||
|
*/
|
||||||
|
public function updateName(int $tactic_id, Account $account): HttpResponse {
|
||||||
|
return Control::runChecked([
|
||||||
|
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()],
|
||||||
|
], function (HttpRequest $request) use ($tactic_id, $account) {
|
||||||
|
|
||||||
|
$failures = $this->model->updateName($tactic_id, $request["name"], $account->getId());
|
||||||
|
|
||||||
|
if (!empty($failures)) {
|
||||||
|
//TODO find a system to handle Unauthorized error codes more easily from failures.
|
||||||
|
return new JsonHttpResponse($failures, HttpCodes::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse::fromCode(HttpCodes::OK);
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||||||||
|
<?php
|
||||||||||||
|
|
||||||||||||
|
namespace IQBall\App;
|
||||||||||||
|
|
||||||||||||
|
use IQBall\Core\Action;
|
||||||||||||
|
use IQBall\Core\Http\HttpResponse;
|
||||||||||||
|
use IQBall\Core\Http\JsonHttpResponse;
|
||||||||||||
|
use IQBall\Core\Session\MutableSessionHandle;
|
||||||||||||
|
use Twig\Environment;
|
||||||||||||
|
use Twig\Error\LoaderError;
|
||||||||||||
|
use Twig\Error\RuntimeError;
|
||||||||||||
|
use Twig\Error\SyntaxError;
|
||||||||||||
|
use Twig\Loader\FilesystemLoader;
|
||||||||||||
|
|
||||||||||||
|
class App {
|
||||||||||||
|
/**
|
||||||||||||
|
* renders (prints out) given HttpResponse to the client
|
||||||||||||
|
* @param HttpResponse $response
|
||||||||||||
|
* @param string $twigViewsFolder
|
||||||||||||
|
* @return void
|
||||||||||||
|
* @throws LoaderError
|
||||||||||||
|
* @throws RuntimeError
|
||||||||||||
|
* @throws SyntaxError
|
||||||||||||
|
*/
|
||||||||||||
|
public static function render(HttpResponse $response, string $twigViewsFolder): void {
|
||||||||||||
|
http_response_code($response->getCode());
|
||||||||||||
|
|
||||||||||||
|
foreach ($response->getHeaders() as $header => $value) {
|
||||||||||||
|
header("$header: $value");
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
if ($response instanceof ViewHttpResponse) {
|
||||||||||||
|
self::renderView($response, $twigViewsFolder);
|
||||||||||||
|
} elseif ($response instanceof JsonHttpResponse) {
|
||||||||||||
|
header('Content-type: application/json');
|
||||||||||||
|
echo $response->getJson();
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* renders (prints out) given ViewHttpResponse to the client
|
||||||||||||
|
* @param ViewHttpResponse $response
|
||||||||||||
|
* @param string $twigViewsFolder
|
||||||||||||
|
* @return void
|
||||||||||||
|
* @throws LoaderError
|
||||||||||||
|
* @throws RuntimeError
|
||||||||||||
|
* @throws SyntaxError
|
||||||||||||
|
*/
|
||||||||||||
|
private static function renderView(ViewHttpResponse $response, string $twigViewsFolder): void {
|
||||||||||||
|
$file = $response->getFile();
|
||||||||||||
|
$args = $response->getArguments();
|
||||||||||||
|
|
||||||||||||
|
switch ($response->getViewKind()) {
|
||||||||||||
|
case ViewHttpResponse::REACT_VIEW:
|
||||||||||||
|
send_react_front($file, $args);
|
||||||||||||
|
break;
|
||||||||||||
|
case ViewHttpResponse::TWIG_VIEW:
|
||||||||||||
|
try {
|
||||||||||||
|
$fl = new FilesystemLoader($twigViewsFolder);
|
||||||||||||
|
$twig = new Environment($fl);
|
||||||||||||
|
$twig->display($file, $args);
|
||||||||||||
|
} catch (RuntimeError | SyntaxError | LoaderError $e) {
|
||||||||||||
|
http_response_code(500);
|
||||||||||||
|
echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s");
|
||||||||||||
|
throw $e;
|
||||||||||||
|
}
|
||||||||||||
|
break;
|
||||||||||||
|
}
|
||||||||||||
maxime.batista marked this conversation as resolved
|
|||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* run a user action, and return the generated response
|
||||||||||||
|
* @param string $authRoute the route towards an authentication page to response with a redirection
|
||||||||||||
|
* if the run action requires auth but session does not contain a logged-in account.
|
||||||||||||
|
* @param Action<MutableSessionHandle> $action
|
||||||||||||
|
* @param mixed[] $params
|
||||||||||||
|
* @param MutableSessionHandle $session
|
||||||||||||
|
* @return HttpResponse
|
||||||||||||
|
*/
|
||||||||||||
|
public static function runAction(string $authRoute, Action $action, array $params, MutableSessionHandle $session): HttpResponse {
|
||||||||||||
|
if ($action->isAuthRequired()) {
|
||||||||||||
|
$account = $session->getAccount();
|
||||||||||||
|
if ($account == null) {
|
||||||||||||
maxime.batista marked this conversation as resolved
|
|||||||||||||
|
// put in the session the initial url the user wanted to get
|
||||||||||||
|
$session->setInitialTarget($_SERVER['REQUEST_URI']);
|
||||||||||||
|
return HttpResponse::redirect($authRoute);
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return $action->run($params, $session);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\App\Controller;
|
||||||
|
|
||||||
|
use IQBall\Core\Http\HttpRequest;
|
||||||
|
use IQBall\Core\Http\HttpResponse;
|
||||||
|
use IQBall\App\ViewHttpResponse;
|
||||||
|
use IQBall\Core\Model\AuthModel;
|
||||||
|
use IQBall\Core\Session\MutableSessionHandle;
|
||||||
|
use IQBall\Core\Validation\ValidationFail;
|
||||||
|
use IQBall\Core\Validation\Validators;
|
||||||
|
|
||||||
|
class AuthController {
|
||||||
|
private AuthModel $model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param AuthModel $model
|
||||||
|
*/
|
||||||
|
public function __construct(AuthModel $model) {
|
||||||
|
$this->model = $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function displayRegister(): HttpResponse {
|
||||||
|
return ViewHttpResponse::twig("display_register.html.twig", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* registers given account
|
||||||
|
* @param mixed[] $request
|
||||||
|
* @param MutableSessionHandle $session
|
||||||
|
* @return HttpResponse
|
||||||
|
*/
|
||||||
|
public function register(array $request, MutableSessionHandle $session): HttpResponse {
|
||||||
|
$fails = [];
|
||||||
|
$request = HttpRequest::from($request, $fails, [
|
||||||
|
"username" => [Validators::name(), Validators::lenBetween(2, 32)],
|
||||||
|
"password" => [Validators::lenBetween(6, 256)],
|
||||||
|
"confirmpassword" => [Validators::lenBetween(6, 256)],
|
||||||
|
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
|
||||||
|
]);
|
||||||
|
if (!empty($fails)) {
|
||||||
|
return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]);
|
||||||
|
}
|
||||||
|
$account = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email'], $fails);
|
||||||
|
if (!empty($fails)) {
|
||||||
|
return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$session->setAccount($account);
|
||||||
|
|
||||||
|
$target_url = $session->getInitialTarget();
|
||||||
|
return HttpResponse::redirect($target_url ?? "/home");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function displayLogin(): HttpResponse {
|
||||||
|
return ViewHttpResponse::twig("display_login.html.twig", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* logins given account credentials
|
||||||
|
* @param mixed[] $request
|
||||||
|
* @param MutableSessionHandle $session
|
||||||
|
* @return HttpResponse
|
||||||
|
*/
|
||||||
|
public function login(array $request, MutableSessionHandle $session): HttpResponse {
|
||||||
|
$fails = [];
|
||||||
|
$request = HttpRequest::from($request, $fails, [
|
||||||
|
"password" => [Validators::lenBetween(6, 256)],
|
||||||
|
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
|
||||||
|
]);
|
||||||
|
if (!empty($fails)) {
|
||||||
|
return ViewHttpResponse::twig("display_login.html.twig", ['fails' => $fails]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = $this->model->login($request['email'], $request['password'], $fails);
|
||||||
|
if (!empty($fails)) {
|
||||||
|
return ViewHttpResponse::twig("display_login.html.twig", ['fails' => $fails]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$session->setAccount($account);
|
||||||
|
|
||||||
|
$target_url = $session->getInitialTarget();
|
||||||
|
$session->setInitialTarget(null);
|
||||||
|
return HttpResponse::redirect($target_url ?? "/home");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\App\Controller;
|
||||||
|
|
||||||
|
use IQBall\Core\Data\TacticInfo;
|
||||||
|
use IQBall\Core\Http\HttpCodes;
|
||||||
|
use IQBall\Core\Http\HttpResponse;
|
||||||
|
use IQBall\App\ViewHttpResponse;
|
||||||
|
use IQBall\Core\Model\TacticModel;
|
||||||
|
use IQBall\Core\Session\SessionHandle;
|
||||||
|
use IQBall\Core\Validator\TacticValidator;
|
||||||
|
|
||||||
|
class EditorController {
|
||||||
|
private TacticModel $model;
|
||||||
|
|
||||||
|
public function __construct(TacticModel $model) {
|
||||||
|
$this->model = $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TacticInfo $tactic
|
||||||
|
* @return ViewHttpResponse the editor view for given tactic
|
||||||
|
*/
|
||||||
|
private function openEditorFor(TacticInfo $tactic): ViewHttpResponse {
|
||||||
|
return ViewHttpResponse::react("views/Editor.tsx", ["name" => $tactic->getName(), "id" => $tactic->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates a new empty tactic, with default name
|
||||||
|
* @param SessionHandle $session
|
||||||
|
* @return ViewHttpResponse the editor view
|
||||||
|
*/
|
||||||
|
public function createNew(SessionHandle $session): ViewHttpResponse {
|
||||||
|
$tactic = $this->model->makeNewDefault($session->getAccount()->getId());
|
||||||
|
return $this->openEditorFor($tactic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns an editor view for a given tactic
|
||||||
|
* @param int $id the targeted tactic identifier
|
||||||
|
* @param SessionHandle $session
|
||||||
|
* @return ViewHttpResponse
|
||||||
|
*/
|
||||||
|
public function openEditor(int $id, SessionHandle $session): ViewHttpResponse {
|
||||||
|
$tactic = $this->model->get($id);
|
||||||
|
|
||||||
|
$failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId());
|
||||||
|
|
||||||
|
if ($failure != null) {
|
||||||
|
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->openEditorFor($tactic);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,155 @@
|
|||||||||||||
|
<?php
|
||||||||||||
|
|
||||||||||||
|
namespace IQBall\App\Controller;
|
||||||||||||
|
|
||||||||||||
|
use IQBall\Core\Http\HttpRequest;
|
||||||||||||
|
use IQBall\Core\Http\HttpResponse;
|
||||||||||||
|
use IQBall\App\ViewHttpResponse;
|
||||||||||||
|
use IQBall\Core\Model\TeamModel;
|
||||||||||||
|
use IQBall\Core\Session\SessionHandle;
|
||||||||||||
|
use IQBall\Core\Validation\FieldValidationFail;
|
||||||||||||
|
use IQBall\Core\Validation\Validators;
|
||||||||||||
|
|
||||||||||||
|
class TeamController {
|
||||||||||||
|
private TeamModel $model;
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param TeamModel $model
|
||||||||||||
|
*/
|
||||||||||||
|
public function __construct(TeamModel $model) {
|
||||||||||||
|
$this->model = $model;
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param SessionHandle $session
|
||||||||||||
|
* @return ViewHttpResponse the team creation panel
|
||||||||||||
|
*/
|
||||||||||||
|
public function displayCreateTeam(SessionHandle $session): ViewHttpResponse {
|
||||||||||||
|
return ViewHttpResponse::twig("insert_team.html.twig", []);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param SessionHandle $session
|
||||||||||||
|
* @return ViewHttpResponse the team panel to add a member
|
||||||||||||
|
*/
|
||||||||||||
|
public function displayAddMember(SessionHandle $session): ViewHttpResponse {
|
||||||||||||
|
return ViewHttpResponse::twig("add_member.html.twig", []);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param SessionHandle $session
|
||||||||||||
|
* @return ViewHttpResponse the team panel to delete a member
|
||||||||||||
|
*/
|
||||||||||||
|
public function displayDeleteMember(SessionHandle $session): ViewHttpResponse {
|
||||||||||||
|
return ViewHttpResponse::twig("delete_member.html.twig", []);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* create a new team from given request name, mainColor, secondColor and picture url
|
||||||||||||
|
* @param array<string, mixed> $request
|
||||||||||||
|
* @param SessionHandle $session
|
||||||||||||
|
* @return HttpResponse
|
||||||||||||
|
*/
|
||||||||||||
|
public function submitTeam(array $request, SessionHandle $session): HttpResponse {
|
||||||||||||
|
$failures = [];
|
||||||||||||
|
$request = HttpRequest::from($request, $failures, [
|
||||||||||||
|
"name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()],
|
||||||||||||
|
"main_color" => [Validators::hexColor()],
|
||||||||||||
|
"second_color" => [Validators::hexColor()],
|
||||||||||||
|
"picture" => [Validators::isURL()],
|
||||||||||||
|
]);
|
||||||||||||
|
if (!empty($failures)) {
|
||||||||||||
|
$badFields = [];
|
||||||||||||
|
foreach ($failures as $e) {
|
||||||||||||
|
if ($e instanceof FieldValidationFail) {
|
||||||||||||
|
$badFields[] = $e->getFieldName();
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]);
|
||||||||||||
|
}
|
||||||||||||
|
$teamId = $this->model->createTeam($request['name'], $request['picture'], $request['main_color'], $request['second_color']);
|
||||||||||||
|
return $this->displayTeam($teamId, $session);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param SessionHandle $session
|
||||||||||||
|
* @return ViewHttpResponse the panel to search a team by its name
|
||||||||||||
|
*/
|
||||||||||||
|
public function displayListTeamByName(SessionHandle $session): ViewHttpResponse {
|
||||||||||||
|
return ViewHttpResponse::twig("list_team_by_name.html.twig", []);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* returns a view that contains all the teams description whose name matches the given name needle.
|
||||||||||||
|
* @param array<string, mixed> $request
|
||||||||||||
|
* @param SessionHandle $session
|
||||||||||||
|
* @return HttpResponse
|
||||||||||||
|
*/
|
||||||||||||
|
public function listTeamByName(array $request, SessionHandle $session): HttpResponse {
|
||||||||||||
|
$errors = [];
|
||||||||||||
|
$request = HttpRequest::from($request, $errors, [
|
||||||||||||
|
"name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()],
|
||||||||||||
|
]);
|
||||||||||||
|
|
||||||||||||
|
if (!empty($errors) && $errors[0] instanceof FieldValidationFail) {
|
||||||||||||
|
$badField = $errors[0]->getFieldName();
|
||||||||||||
|
return ViewHttpResponse::twig('list_team_by_name.html.twig', ['bad_field' => $badField]);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
$teams = $this->model->listByName($request['name']);
|
||||||||||||
|
|
||||||||||||
|
if (empty($teams)) {
|
||||||||||||
|
return ViewHttpResponse::twig('display_teams.html.twig', []);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $teams]);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param int $id
|
||||||||||||
|
* @param SessionHandle $session
|
||||||||||||
|
* @return ViewHttpResponse a view that displays given team information
|
||||||||||||
|
*/
|
||||||||||||
|
public function displayTeam(int $id, SessionHandle $session): ViewHttpResponse {
|
||||||||||||
|
$result = $this->model->getTeam($id);
|
||||||||||||
|
return ViewHttpResponse::twig('display_team.html.twig', ['team' => $result]);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
maxime.batista marked this conversation as resolved
|
|||||||||||||
|
* add a member to a team
|
||||||||||||
|
* @param array<string, mixed> $request
|
||||||||||||
|
* @param SessionHandle $session
|
||||||||||||
|
* @return HttpResponse
|
||||||||||||
|
*/
|
||||||||||||
|
public function addMember(array $request, SessionHandle $session): HttpResponse {
|
||||||||||||
|
$errors = [];
|
||||||||||||
|
|
||||||||||||
|
$request = HttpRequest::from($request, $errors, [
|
||||||||||||
|
"team" => [Validators::isInteger()],
|
||||||||||||
|
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
|
||||||||||||
|
]);
|
||||||||||||
|
|
||||||||||||
|
$teamId = intval($request['team']);
|
||||||||||||
|
$this->model->addMember($request['email'], $teamId, $request['role']);
|
||||||||||||
|
return $this->displayTeam($teamId, $session);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* remove a member from a team
|
||||||||||||
|
* @param array<string, mixed> $request
|
||||||||||||
|
* @param SessionHandle $session
|
||||||||||||
|
* @return HttpResponse
|
||||||||||||
|
*/
|
||||||||||||
|
public function deleteMember(array $request, SessionHandle $session): HttpResponse {
|
||||||||||||
|
$errors = [];
|
||||||||||||
|
|
||||||||||||
|
$request = HttpRequest::from($request, $errors, [
|
||||||||||||
|
"team" => [Validators::isInteger()],
|
||||||||||||
|
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
|
||||||||||||
|
]);
|
||||||||||||
|
|
||||||||||||
|
return $this->displayTeam($this->model->deleteMember($request['email'], intval($request['team'])), $session);
|
||||||||||||
|
}
|
||||||||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\App\Controller;
|
||||||
|
|
||||||
|
use IQBall\Core\Http\HttpResponse;
|
||||||
|
use IQBall\App\ViewHttpResponse;
|
||||||
|
use IQBall\Core\Model\TacticModel;
|
||||||
|
use IQBall\Core\Session\SessionHandle;
|
||||||
|
|
||||||
|
class UserController {
|
||||||
|
private TacticModel $tactics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TacticModel $tactics
|
||||||
|
*/
|
||||||
|
public function __construct(TacticModel $tactics) {
|
||||||
|
$this->tactics = $tactics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param SessionHandle $session
|
||||||
|
* @return ViewHttpResponse the home page view
|
||||||
|
*/
|
||||||
|
public function home(SessionHandle $session): ViewHttpResponse {
|
||||||
|
//TODO use session's account to get the last 5 tactics of the logged-in account
|
||||||
|
$listTactic = $this->tactics->getLast(5);
|
||||||
|
return ViewHttpResponse::twig("home.twig", ["recentTactic" => $listTactic]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ViewHttpResponse account settings page
|
||||||
|
*/
|
||||||
|
public function settings(SessionHandle $session): ViewHttpResponse {
|
||||||
|
return ViewHttpResponse::twig("account_settings.twig", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\App\Controller;
|
||||||
|
|
||||||
|
use IQBall\Core\Http\HttpCodes;
|
||||||
|
use IQBall\Core\Http\HttpResponse;
|
||||||
|
use IQBall\App\ViewHttpResponse;
|
||||||
|
use IQBall\Core\Model\TacticModel;
|
||||||
|
use IQBall\Core\Session\SessionHandle;
|
||||||
|
use IQBall\Core\Validator\TacticValidator;
|
||||||
|
|
||||||
|
class VisualizerController {
|
||||||
|
private TacticModel $tacticModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TacticModel $tacticModel
|
||||||
|
*/
|
||||||
|
public function __construct(TacticModel $tacticModel) {
|
||||||
|
$this->tacticModel = $tacticModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* opens a visualisation page for the tactic specified by its identifier in the url.
|
||||||
|
* @param int $id
|
||||||
|
* @param SessionHandle $session
|
||||||
|
* @return HttpResponse
|
||||||
|
*/
|
||||||
|
public function openVisualizer(int $id, SessionHandle $session): HttpResponse {
|
||||||
|
$tactic = $this->tacticModel->get($id);
|
||||||
|
|
||||||
|
$failure = TacticValidator::validateAccess($tactic, $id, $session->getAccount()->getId());
|
||||||
|
|
||||||
|
if ($failure != null) {
|
||||||
|
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Paramètres</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding-left: 10%;
|
||||||
|
padding-right: 10%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<button onclick="location.pathname='/home'">Retour</button>
|
||||||
|
<h1>Paramètres</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,103 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Ajouter un membre</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="radio"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"] {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"]:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role{
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>Ajouter un membre à votre équipe</h2>
|
||||||
|
<form action="/team/members/add" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="team">Team où ajouter le membre :</label>
|
||||||
|
<input type="text" id="team" name="team" required>
|
||||||
|
<label for="mail">Email du membre :</label>
|
||||||
|
<input type="text" id="mail" name="mail" required>
|
||||||
|
|
||||||
|
<fieldset class="role">
|
||||||
|
<legend >Rôle du membre dans l'équipe :</legend>
|
||||||
|
<div class="radio">
|
||||||
|
<label for="P">Joueur</label>
|
||||||
|
<input type="radio" id="P" name="role" value="P" checked />
|
||||||
|
</div>
|
||||||
|
<div class="radio">
|
||||||
|
<label for="C">Coach</label>
|
||||||
|
<input type="radio" id="C" name="role" value="C" />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="submit" value="Confirmer">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,73 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Ajouter un membre</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"] {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"]:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>Supprimez un membre de votre équipe</h2>
|
||||||
|
<form action="/team/members/remove" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="team">Team où supprimer le membre :</label>
|
||||||
|
<input type="text" id="team" name="team" required>
|
||||||
|
<label for="mail">Email du membre :</label>
|
||||||
|
<input type="text" id="mail" name="mail" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="submit" value="Confirmer">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,94 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Page d'accueil</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding-left: 10%;
|
||||||
|
padding-right: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bandeau {
|
||||||
|
display : flex;
|
||||||
|
flex-direction : row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bandeau > h1 {
|
||||||
|
self-align : center;
|
||||||
|
padding : 0%;
|
||||||
|
margin : 0%;
|
||||||
|
justify-content : center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#account {
|
||||||
|
display : flex;
|
||||||
|
flex-direction : column;
|
||||||
|
align-content : center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#account:hover {
|
||||||
|
background-color : gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
#account img {
|
||||||
|
width : 70%;
|
||||||
|
height : auto;
|
||||||
|
align-self : center;
|
||||||
|
padding : 5%;
|
||||||
|
margin : 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#account p {
|
||||||
|
align-self : center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="bandeau">
|
||||||
|
<h1>IQ Ball</h1>
|
||||||
|
<div id="account" onclick="location.pathname='/settings'">
|
||||||
|
<img
|
||||||
|
src="../../../front/assets/icon/account.svg"
|
||||||
|
alt="Account logo"
|
||||||
|
/>
|
||||||
|
<p>Mon profil<p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Mes équipes</h2>
|
||||||
|
|
||||||
|
<button onclick="location.pathname='/team/new'"> Créer une nouvelle équipe </button>
|
||||||
|
|
||||||
|
{% if recentTeam != null %}
|
||||||
|
{% for team in recentTeam %}
|
||||||
|
<div>
|
||||||
|
<p> {{team.name}} </p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p>Aucune équipe créé !</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h2> Mes strategies </h2>
|
||||||
|
|
||||||
|
<button onclick="location.pathname='/tactic/new'"> Créer une nouvelle tactique </button>
|
||||||
|
|
||||||
|
{% if recentTactic != null %}
|
||||||
|
{% for tactic in recentTactic %}
|
||||||
|
<div onclick="location.pathname=/tactic/{{ strategie.id }}/edit">
|
||||||
|
<p> {{tactic.id}} - {{tactic.name}} - {{tactic.creation_date}} </p>
|
||||||
|
<button onclick="location.pathname='/tactic/{{ tactic.id }}/edit'"> Editer la stratégie {{tactic.id}} </button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p> Aucune tactique créé !</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,55 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controller\Api;
|
|
||||||
|
|
||||||
use App\Controller\Control;
|
|
||||||
use App\Http\HttpCodes;
|
|
||||||
use App\Http\HttpRequest;
|
|
||||||
use App\Http\HttpResponse;
|
|
||||||
use App\Http\JsonHttpResponse;
|
|
||||||
use App\Model\TacticModel;
|
|
||||||
use App\Validation\Validators;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API endpoint related to tactics
|
|
||||||
*/
|
|
||||||
class APITacticController {
|
|
||||||
private TacticModel $model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param TacticModel $model
|
|
||||||
*/
|
|
||||||
public function __construct(TacticModel $model) {
|
|
||||||
$this->model = $model;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateName(int $tactic_id): HttpResponse {
|
|
||||||
return Control::runChecked([
|
|
||||||
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()],
|
|
||||||
], function (HttpRequest $request) use ($tactic_id) {
|
|
||||||
$this->model->updateName($tactic_id, $request["name"]);
|
|
||||||
return HttpResponse::fromCode(HttpCodes::OK);
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function newTactic(): HttpResponse {
|
|
||||||
return Control::runChecked([
|
|
||||||
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()],
|
|
||||||
], function (HttpRequest $request) {
|
|
||||||
$tactic = $this->model->makeNew($request["name"]);
|
|
||||||
$id = $tactic->getId();
|
|
||||||
return new JsonHttpResponse(["id" => $id]);
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTacticInfo(int $id): HttpResponse {
|
|
||||||
$tactic_info = $this->model->get($id);
|
|
||||||
|
|
||||||
if ($tactic_info == null) {
|
|
||||||
return new JsonHttpResponse("could not find tactic #$id", HttpCodes::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JsonHttpResponse($tactic_info);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controller;
|
|
||||||
|
|
||||||
use App\Gateway\AuthGateway;
|
|
||||||
use App\Http\HttpRequest;
|
|
||||||
use App\Http\HttpResponse;
|
|
||||||
use App\Http\ViewHttpResponse;
|
|
||||||
use App\Model\AuthModel;
|
|
||||||
use App\Validation\FieldValidationFail;
|
|
||||||
use App\Validation\ValidationFail;
|
|
||||||
use App\Validation\Validators;
|
|
||||||
use Twig\Environment;
|
|
||||||
|
|
||||||
class AuthController {
|
|
||||||
private AuthModel $model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param AuthModel $model
|
|
||||||
*/
|
|
||||||
public function __construct(AuthModel $model) {
|
|
||||||
$this->model = $model;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function displayRegister(): HttpResponse {
|
|
||||||
return ViewHttpResponse::twig("display_register.html.twig", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $viewName
|
|
||||||
* @param ValidationFail[] $fails
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
private function displayBadFields(string $viewName, array $fails): HttpResponse {
|
|
||||||
$bad_fields = [];
|
|
||||||
foreach ($fails as $err) {
|
|
||||||
if ($err instanceof FieldValidationFail) {
|
|
||||||
$bad_fields[] = $err->getFieldName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ViewHttpResponse::twig($viewName, ['bad_fields' => $bad_fields]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mixed[] $request
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function confirmRegister(array $request): HttpResponse {
|
|
||||||
$fails = [];
|
|
||||||
$request = HttpRequest::from($request, $fails, [
|
|
||||||
"username" => [Validators::name(), Validators::lenBetween(2, 32)],
|
|
||||||
"password" => [Validators::lenBetween(6, 256)],
|
|
||||||
"confirmpassword" => [Validators::lenBetween(6, 256)],
|
|
||||||
"email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)],
|
|
||||||
]);
|
|
||||||
if (!empty($fails)) {
|
|
||||||
return $this->displayBadFields("display_register.html.twig", $fails);
|
|
||||||
}
|
|
||||||
$fails = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email']);
|
|
||||||
if (empty($fails)) {
|
|
||||||
$results = $this->model->getUserFields($request['email']);
|
|
||||||
return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]);
|
|
||||||
}
|
|
||||||
return $this->displayBadFields("display_register.html.twig", $fails);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function displayLogin(): HttpResponse {
|
|
||||||
return ViewHttpResponse::twig("display_login.html.twig", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mixed[] $request
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function confirmLogin(array $request): HttpResponse {
|
|
||||||
$fails = [];
|
|
||||||
$request = HttpRequest::from($request, $fails, [
|
|
||||||
"password" => [Validators::lenBetween(6, 256)],
|
|
||||||
"email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)],
|
|
||||||
]);
|
|
||||||
if (!empty($fails)) {
|
|
||||||
return $this->displayBadFields("display_login.html.twig", $fails);
|
|
||||||
}
|
|
||||||
|
|
||||||
$fails = $this->model->login($request['email'], $request['password']);
|
|
||||||
if (empty($fails)) {
|
|
||||||
$results = $this->model->getUserFields($request['email']);
|
|
||||||
return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]);
|
|
||||||
}
|
|
||||||
return $this->displayBadFields("display_login.html.twig", $fails);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controller;
|
|
||||||
|
|
||||||
use App\Data\TacticInfo;
|
|
||||||
use App\Http\HttpCodes;
|
|
||||||
use App\Http\HttpRequest;
|
|
||||||
use App\Http\HttpResponse;
|
|
||||||
use App\Http\JsonHttpResponse;
|
|
||||||
use App\Http\ViewHttpResponse;
|
|
||||||
use App\Model\TacticModel;
|
|
||||||
|
|
||||||
class EditorController {
|
|
||||||
private TacticModel $model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param TacticModel $model
|
|
||||||
*/
|
|
||||||
public function __construct(TacticModel $model) {
|
|
||||||
$this->model = $model;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function openEditor(TacticInfo $tactic): HttpResponse {
|
|
||||||
return ViewHttpResponse::react("views/Editor.tsx", ["name" => $tactic->getName(), "id" => $tactic->getId()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function makeNew(): HttpResponse {
|
|
||||||
$tactic = $this->model->makeNewDefault();
|
|
||||||
return $this->openEditor($tactic);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns an editor view for a given tactic
|
|
||||||
* @param int $id the targeted tactic identifier
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function openEditorFor(int $id): HttpResponse {
|
|
||||||
$tactic = $this->model->get($id);
|
|
||||||
|
|
||||||
if ($tactic == null) {
|
|
||||||
return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->openEditor($tactic);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controller;
|
|
||||||
|
|
||||||
require_once __DIR__ . "/../react-display.php";
|
|
||||||
|
|
||||||
use App\Validation\ValidationFail;
|
|
||||||
use Twig\Environment;
|
|
||||||
use Twig\Error\LoaderError;
|
|
||||||
use Twig\Error\RuntimeError;
|
|
||||||
use Twig\Error\SyntaxError;
|
|
||||||
|
|
||||||
class ErrorController {
|
|
||||||
/**
|
|
||||||
* @param ValidationFail[] $failures
|
|
||||||
* @param Environment $twig
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function displayFailures(array $failures, Environment $twig): void {
|
|
||||||
try {
|
|
||||||
$twig->display("error.html.twig", ['failures' => $failures]);
|
|
||||||
} catch (LoaderError|RuntimeError|SyntaxError $e) {
|
|
||||||
echo "Twig error: $e";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controller;
|
|
||||||
|
|
||||||
require_once __DIR__ . "/../react-display.php";
|
|
||||||
|
|
||||||
use App\Gateway\FormResultGateway;
|
|
||||||
use App\Http\HttpRequest;
|
|
||||||
use App\Http\HttpResponse;
|
|
||||||
use App\Http\ViewHttpResponse;
|
|
||||||
use App\Validation\Validators;
|
|
||||||
|
|
||||||
class SampleFormController {
|
|
||||||
private FormResultGateway $gateway;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param FormResultGateway $gateway
|
|
||||||
*/
|
|
||||||
public function __construct(FormResultGateway $gateway) {
|
|
||||||
$this->gateway = $gateway;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function displayFormReact(): HttpResponse {
|
|
||||||
return ViewHttpResponse::react("views/SampleForm.tsx", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function displayFormTwig(): HttpResponse {
|
|
||||||
return ViewHttpResponse::twig('sample_form.html.twig', []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $form
|
|
||||||
* @param callable(array<array<string, string>>): ViewHttpResponse $response
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
private function submitForm(array $form, callable $response): HttpResponse {
|
|
||||||
return Control::runCheckedFrom($form, [
|
|
||||||
"name" => [Validators::lenBetween(0, 32), Validators::name("Le nom ne peut contenir que des lettres, des chiffres et des accents")],
|
|
||||||
"description" => [Validators::lenBetween(0, 512)],
|
|
||||||
], function (HttpRequest $req) use ($response) {
|
|
||||||
$description = htmlspecialchars($req["description"]);
|
|
||||||
$this->gateway->insert($req["name"], $description);
|
|
||||||
$results = ["results" => $this->gateway->listResults()];
|
|
||||||
return call_user_func_array($response, [$results]);
|
|
||||||
}, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $form
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function submitFormTwig(array $form): HttpResponse {
|
|
||||||
return $this->submitForm($form, fn(array $results) => ViewHttpResponse::twig('display_results.html.twig', $results));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $form
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function submitFormReact(array $form): HttpResponse {
|
|
||||||
return $this->submitForm($form, fn(array $results) => ViewHttpResponse::react('views/DisplayResults.tsx', $results));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controller;
|
|
||||||
|
|
||||||
use App\Http\HttpRequest;
|
|
||||||
use App\Http\HttpResponse;
|
|
||||||
use App\Http\ViewHttpResponse;
|
|
||||||
use App\Model\TeamModel;
|
|
||||||
use App\Validation\FieldValidationFail;
|
|
||||||
use App\Validation\Validators;
|
|
||||||
|
|
||||||
class TeamController {
|
|
||||||
private TeamModel $model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param TeamModel $model
|
|
||||||
*/
|
|
||||||
public function __construct(TeamModel $model) {
|
|
||||||
$this->model = $model;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function displaySubmitTeam(): HttpResponse {
|
|
||||||
return ViewHttpResponse::twig("insert_team.html.twig", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $request
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function submitTeam(array $request): HttpResponse {
|
|
||||||
$errors = [];
|
|
||||||
|
|
||||||
$request = HttpRequest::from($request, $errors, [
|
|
||||||
"name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()],
|
|
||||||
"mainColor" => [Validators::regex('/#(?:[0-9a-fA-F]{6})/')],
|
|
||||||
"secondColor" => [Validators::regex('/#(?:[0-9a-fA-F]{6})/')],
|
|
||||||
"picture" => [Validators::isURL()],
|
|
||||||
]);
|
|
||||||
if (!empty($errors)) {
|
|
||||||
$badFields = [];
|
|
||||||
foreach ($errors as $e) {
|
|
||||||
if ($e instanceof FieldValidationFail) {
|
|
||||||
$badFields[] = $e->getFieldName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ViewHttpResponse::twig('insert_team.html.twig', ['bad_fields' => $badFields]);
|
|
||||||
}
|
|
||||||
return $this->getTeam($this->model->createTeam($request['name'], $request['picture'], $request['mainColor'], $request['secondColor']));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function displayListTeamByName(): HttpResponse {
|
|
||||||
return ViewHttpResponse::twig("list_team_by_name.html.twig", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string , mixed> $request
|
|
||||||
* @return HttpResponse
|
|
||||||
*/
|
|
||||||
public function listTeamByName(array $request): HttpResponse {
|
|
||||||
$errors = [];
|
|
||||||
$request = HttpRequest::from($request, $errors, [
|
|
||||||
"name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!empty($errors) && $errors[0] instanceof FieldValidationFail) {
|
|
||||||
$badField = $errors[0]->getFieldName();
|
|
||||||
return ViewHttpResponse::twig('list_team_by_name.html.twig', ['bad_field' => $badField]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$results = $this->model->listByName($request['name']);
|
|
||||||
|
|
||||||
if (empty($results)) {
|
|
||||||
return ViewHttpResponse::twig('display_teams.html.twig', []);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ViewHttpResponse::twig('display_teams.html.twig', ['teams' => $results]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTeam(int $id): HttpResponse {
|
|
||||||
$result = $this->model->displayTeam($id);
|
|
||||||
return ViewHttpResponse::twig('display_team.html.twig', ['team' => $result]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controller;
|
|
||||||
|
|
||||||
use App\Http\HttpCodes;
|
|
||||||
use App\Http\HttpResponse;
|
|
||||||
use App\Http\JsonHttpResponse;
|
|
||||||
use App\Http\ViewHttpResponse;
|
|
||||||
use App\Model\TacticModel;
|
|
||||||
|
|
||||||
class VisualizerController {
|
|
||||||
private TacticModel $tacticModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param TacticModel $tacticModel
|
|
||||||
*/
|
|
||||||
|
|
||||||
public function __construct(TacticModel $tacticModel) {
|
|
||||||
$this->tacticModel = $tacticModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function openVisualizer(int $id): HttpResponse {
|
|
||||||
$tactic = $this->tacticModel->get($id);
|
|
||||||
|
|
||||||
if ($tactic == null) {
|
|
||||||
return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,58 @@
|
|||||||||||||
|
<?php
|
||||||||||||
|
|
||||||||||||
|
namespace IQBall\Core;
|
||||||||||||
|
|
||||||||||||
|
use IQBall\Core\Http\HttpResponse;
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* Represent an action.
|
||||||||||||
|
* @template S session
|
||||||||||||
|
*/
|
||||||||||||
|
class Action {
|
||||||||||||
|
/**
|
||||||||||||
|
* @var callable(mixed[], S): HttpResponse $action action to call
|
||||||||||||
|
*/
|
||||||||||||
|
protected $action;
|
||||||||||||
|
|
||||||||||||
|
private bool $isAuthRequired;
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param callable(mixed[], S): HttpResponse $action
|
||||||||||||
|
*/
|
||||||||||||
|
protected function __construct(callable $action, bool $isAuthRequired) {
|
||||||||||||
|
$this->action = $action;
|
||||||||||||
|
$this->isAuthRequired = $isAuthRequired;
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
public function isAuthRequired(): bool {
|
||||||||||||
|
return $this->isAuthRequired;
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* Runs an action
|
||||||||||||
|
* @param mixed[] $params
|
||||||||||||
|
* @param S $session
|
||||||||||||
|
* @return HttpResponse
|
||||||||||||
maxime.batista marked this conversation as resolved
|
|||||||||||||
|
*/
|
||||||||||||
|
public function run(array $params, $session): HttpResponse {
|
||||||||||||
|
$params = array_values($params);
|
||||||||||||
|
$params[] = $session;
|
||||||||||||
|
return call_user_func_array($this->action, $params);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param callable(mixed[], S): HttpResponse $action
|
||||||||||||
|
* @return Action<S> an action that does not require to have an authorization.
|
||||||||||||
maxime.batista marked this conversation as resolved
|
|||||||||||||
|
*/
|
||||||||||||
|
public static function noAuth(callable $action): Action {
|
||||||||||||
|
return new Action($action, false);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param callable(mixed[], S): HttpResponse $action
|
||||||||||||
|
* @return Action<S> an action that does require to have an authorization.
|
||||||||||||
maxime.batista marked this conversation as resolved
|
|||||||||||||
|
*/
|
||||||||||||
|
public static function auth(callable $action): Action {
|
||||||||||||
|
return new Action($action, true);
|
||||||||||||
|
}
|
||||||||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App;
|
namespace IQBall\Core;
|
||||||
|
|
||||||
use PDO;
|
use PDO;
|
||||||
|
|
||||||
class Connexion {
|
class Connection {
|
||||||
private PDO $pdo;
|
private PDO $pdo;
|
||||||
|
|
||||||
/**
|
/**
|
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\Core\Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class of a user account.
|
||||||
|
* Contains the private information that we don't want
|
||||||
|
* to share to other users, or non-needed public information
|
||||||
|
*/
|
||||||
|
class Account {
|
||||||
|
/**
|
||||||
|
* @var string $email account's mail address
|
||||||
|
*/
|
||||||
|
private string $email;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string string token
|
||||||
|
*/
|
||||||
|
private string $token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string the account's username
|
||||||
|
*/
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private int $id;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $email
|
||||||
|
* @param string $name
|
||||||
|
* @param string $token
|
||||||
|
* @param int $id
|
||||||
|
*/
|
||||||
|
public function __construct(string $email, string $name, string $token, int $id) {
|
||||||
|
$this->email = $email;
|
||||||
|
$this->name = $name;
|
||||||
|
$this->token = $token;
|
||||||
|
$this->id = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): int {
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmail(): string {
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getToken(): string {
|
||||||
|
return $this->token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string {
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\Core\Data;
|
||||||
|
|
||||||
|
class Team {
|
||||||
|
private TeamInfo $info;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Member[] maps users with their role
|
||||||
|
*/
|
||||||
|
private array $members;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TeamInfo $info
|
||||||
|
* @param Member[] $members
|
||||||
|
*/
|
||||||
|
public function __construct(TeamInfo $info, array $members = []) {
|
||||||
|
$this->info = $info;
|
||||||
|
$this->members = $members;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInfo(): TeamInfo {
|
||||||
|
return $this->info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Member[]
|
||||||
|
*/
|
||||||
|
public function listMembers(): array {
|
||||||
|
return $this->members;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||||||||
|
<?php
|
||||||||||||
|
|
||||||||||||
|
namespace IQBall\Core\Gateway;
|
||||||||||||
|
|
||||||||||||
|
use IQBall\Core\Connection;
|
||||||||||||
|
use IQBall\Core\Data\Account;
|
||||||||||||
|
use PDO;
|
||||||||||||
|
|
||||||||||||
|
class AccountGateway {
|
||||||||||||
|
private Connection $con;
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param Connection $con
|
||||||||||||
|
*/
|
||||||||||||
|
public function __construct(Connection $con) {
|
||||||||||||
|
$this->con = $con;
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
|
public function insertAccount(string $name, string $email, string $token, string $hash): int {
|
||||||||||||
maxime.batista marked this conversation as resolved
|
|||||||||||||
|
$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;
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param string $email
|
||||||||||||
|
* @return string|null the hashed user's password, or null if the given mail does not exist
|
||||||||||||
|
*/
|
||||||||||||
|
public function getHash(string $email): ?string {
|
||||||||||||
|
$results = $this->getRowsFromMail($email);
|
||||||||||||
|
if ($results == null) {
|
||||||||||||
|
return null;
|
||||||||||||
|
}
|
||||||||||||
|
return $results['hash'];
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param string $email
|
||||||||||||
|
* @return bool true if the given email exists in the database
|
||||||||||||
|
*/
|
||||||||||||
|
public function exists(string $email): bool {
|
||||||||||||
|
return $this->getRowsFromMail($email) != null;
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param string $email
|
||||||||||||
|
* @return Account|null
|
||||||||||||
|
*/
|
||||||||||||
|
public function getAccountFromMail(string $email): ?Account {
|
||||||||||||
|
$acc = $this->getRowsFromMail($email);
|
||||||||||||
|
if (empty($acc)) {
|
||||||||||||
maxime.batista marked this conversation as resolved
|
|||||||||||||
|
return null;
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return new Account($email, $acc["username"], $acc["token"], $acc["id"]);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param string $token get an account from given token
|
||||||||||||
|
* @return Account|null
|
||||||||||||
|
*/
|
||||||||||||
|
public function getAccountFromToken(string $token): ?Account {
|
||||||||||||
|
$acc = $this->con->fetch("SELECT * FROM Account WHERE token = :token", [':token' => [$token, PDO::PARAM_STR]])[0] ?? null;
|
||||||||||||
|
if (empty($acc)) {
|
||||||||||||
|
return null;
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return new Account($acc["email"], $acc["username"], $acc["token"], $acc["id"]);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\Core\Gateway;
|
||||||
|
|
||||||
|
use IQBall\Core\Connection;
|
||||||
|
use IQBall\Core\Data\Member;
|
||||||
|
use IQBall\Core\Data\MemberRole;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class MemberGateway {
|
||||||
|
private Connection $con;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Connection $con
|
||||||
|
*/
|
||||||
|
public function __construct(Connection $con) {
|
||||||
|
$this->con = $con;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* insert member to a team
|
||||||
|
* @param int $idTeam
|
||||||
|
* @param int $userId
|
||||||
|
* @param string $role
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function insert(int $idTeam, int $userId, string $role): void {
|
||||||
|
$this->con->exec(
|
||||||
|
"INSERT INTO Member(id_team, id_user, role) VALUES (:id_team, :id_user, :role)",
|
||||||
|
[
|
||||||
|
":id_team" => [$idTeam, PDO::PARAM_INT],
|
||||||
|
":id_user" => [$userId, PDO::PARAM_INT],
|
||||||
|
":role" => [$role, PDO::PARAM_STR],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $teamId
|
||||||
|
* @return Member[]
|
||||||
|
*/
|
||||||
|
public function getMembersOfTeam(int $teamId): array {
|
||||||
|
$rows = $this->con->fetch(
|
||||||
|
"SELECT a.id,m.role,a.email,a.username FROM Account a,Team t,Member m WHERE t.id = :id AND m.id_team = t.id AND m.id_user = a.id",
|
||||||
|
[
|
||||||
|
":id" => [$teamId, PDO::PARAM_INT],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return array_map(fn($row) => new Member($row['id_user'], $row['id_team'], MemberRole::fromName($row['role'])), $rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove member from given team
|
||||||
|
* @param int $idTeam
|
||||||
|
* @param int $idMember
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function remove(int $idTeam, int $idMember): void {
|
||||||
|
$this->con->exec(
|
||||||
|
"DELETE FROM Member WHERE id_team = :id_team AND id_user = :id_user",
|
||||||
|
[
|
||||||
|
":id_team" => [$idTeam, PDO::PARAM_INT],
|
||||||
|
":id_user" => [$idMember, PDO::PARAM_INT],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\Core\Gateway;
|
||||||
|
|
||||||
|
use IQBall\Core\Connection;
|
||||||
|
use IQBall\Core\Data\TacticInfo;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class TacticInfoGateway {
|
||||||
|
private Connection $con;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Connection $con
|
||||||
|
*/
|
||||||
|
public function __construct(Connection $con) {
|
||||||
|
$this->con = $con;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get tactic information from given identifier
|
||||||
|
* @param int $id
|
||||||
|
* @return TacticInfo|null
|
||||||
|
*/
|
||||||
|
public function get(int $id): ?TacticInfo {
|
||||||
|
$res = $this->con->fetch(
|
||||||
|
"SELECT * FROM Tactic WHERE id = :id",
|
||||||
|
[":id" => [$id, PDO::PARAM_INT]]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isset($res[0])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $res[0];
|
||||||
|
|
||||||
|
return new TacticInfo($id, $row["name"], strtotime($row["creation_date"]), $row["owner"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the nb last tactics created
|
||||||
|
*
|
||||||
|
* @param integer $nb
|
||||||
|
* @return array<array<string,mixed>>
|
||||||
|
*/
|
||||||
|
public function getLast(int $nb): ?array {
|
||||||
|
$res = $this->con->fetch(
|
||||||
|
"SELECT * FROM Tactic ORDER BY creation_date DESC LIMIT :nb ",
|
||||||
|
[":nb" => [$nb, PDO::PARAM_INT]]
|
||||||
|
);
|
||||||
|
if (count($res) == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @param int $owner
|
||||||
|
* @return TacticInfo
|
||||||
|
*/
|
||||||
|
public function insert(string $name, int $owner): TacticInfo {
|
||||||
|
$this->con->exec(
|
||||||
|
"INSERT INTO Tactic(name, owner) VALUES(:name, :owner)",
|
||||||
|
[
|
||||||
|
":name" => [$name, PDO::PARAM_STR],
|
||||||
|
":owner" => [$owner, PDO::PARAM_INT],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$row = $this->con->fetch(
|
||||||
|
"SELECT id, creation_date, owner FROM Tactic WHERE :id = id",
|
||||||
|
[':id' => [$this->con->lastInsertId(), PDO::PARAM_INT]]
|
||||||
|
)[0];
|
||||||
|
return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"]), $row["owner"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update name of given tactic identifier
|
||||||
|
* @param int $id
|
||||||
|
* @param string $name
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function updateName(int $id, string $name): void {
|
||||||
|
$this->con->exec(
|
||||||
|
"UPDATE Tactic SET name = :name WHERE id = :id",
|
||||||
|
[
|
||||||
|
":name" => [$name, PDO::PARAM_STR],
|
||||||
|
":id" => [$id, PDO::PARAM_INT],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\Core\Gateway;
|
||||||
|
|
||||||
|
use IQBall\Core\Connection;
|
||||||
|
use IQBall\Core\Data\Color;
|
||||||
|
use IQBall\Core\Data\TeamInfo;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class TeamGateway {
|
||||||
|
private Connection $con;
|
||||||
|
|
||||||
|
public function __construct(Connection $con) {
|
||||||
|
$this->con = $con;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @param string $picture
|
||||||
|
* @param string $mainColor
|
||||||
|
* @param string $secondColor
|
||||||
|
* @return int the inserted team identifier
|
||||||
|
*/
|
||||||
|
public function insert(string $name, string $picture, string $mainColor, string $secondColor): int {
|
||||||
|
$this->con->exec(
|
||||||
|
"INSERT INTO Team(name, picture, main_color, second_color) VALUES (:team_name , :picture, :main_color, :second_color)",
|
||||||
|
[
|
||||||
|
":team_name" => [$name, PDO::PARAM_STR],
|
||||||
|
":picture" => [$picture, PDO::PARAM_STR],
|
||||||
|
":main_color" => [$mainColor, PDO::PARAM_STR],
|
||||||
|
":second_color" => [$secondColor, PDO::PARAM_STR],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return intval($this->con->lastInsertId());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @return TeamInfo[]
|
||||||
|
*/
|
||||||
|
public function listByName(string $name): array {
|
||||||
|
$result = $this->con->fetch(
|
||||||
|
"SELECT * FROM Team WHERE name LIKE '%' || :name || '%'",
|
||||||
|
[
|
||||||
|
":name" => [$name, PDO::PARAM_STR],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return array_map(fn($row) => new TeamInfo($row['id'], $row['name'], $row['picture'], Color::from($row['main_color']), Color::from($row['second_color'])), $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $id
|
||||||
|
* @return TeamInfo
|
||||||
|
*/
|
||||||
|
public function getTeamById(int $id): ?TeamInfo {
|
||||||
|
$row = $this->con->fetch(
|
||||||
|
"SELECT * FROM Team WHERE id = :id",
|
||||||
|
[
|
||||||
|
":id" => [$id, PDO::PARAM_INT],
|
||||||
|
]
|
||||||
|
)[0] ?? null;
|
||||||
|
if ($row == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TeamInfo($row['id'], $row['name'], $row['picture'], Color::from($row['main_color']), Color::from($row['second_color']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public function getTeamIdByName(string $name): ?int {
|
||||||
|
return $this->con->fetch(
|
||||||
|
"SELECT id FROM Team WHERE name = :name",
|
||||||
|
[
|
||||||
|
":name" => [$name, PDO::PARAM_INT],
|
||||||
|
]
|
||||||
|
)[0]['id'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -1,13 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http;
|
namespace IQBall\Core\Http;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class to define constants of used http codes
|
* Utility class to define constants of used http codes
|
||||||
*/
|
*/
|
||||||
class HttpCodes {
|
class HttpCodes {
|
||||||
public const OK = 200;
|
public const OK = 200;
|
||||||
|
public const FOUND = 302;
|
||||||
public const BAD_REQUEST = 400;
|
public const BAD_REQUEST = 400;
|
||||||
|
public const UNAUTHORIZED = 401;
|
||||||
|
|
||||||
|
public const FORBIDDEN = 403;
|
||||||
|
|
||||||
public const NOT_FOUND = 404;
|
public const NOT_FOUND = 404;
|
||||||
|
|
||||||
}
|
}
|
@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http;
|
namespace IQBall\Core\Http;
|
||||||
|
|
||||||
use App\Validation\FieldValidationFail;
|
use IQBall\Core\Validation\FieldValidationFail;
|
||||||
use App\Validation\Validation;
|
use IQBall\Core\Validation\Validation;
|
||||||
use App\Validation\ValidationFail;
|
use IQBall\Core\Validation\ValidationFail;
|
||||||
use App\Validation\Validator;
|
use IQBall\Core\Validation\Validator;
|
||||||
use ArrayAccess;
|
use ArrayAccess;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\Core\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, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $code
|
||||||
|
* @return HttpResponse
|
||||||
|
*/
|
||||||
|
public static function fromCode(int $code): HttpResponse {
|
||||||
|
return new HttpResponse($code, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $url the url to redirect
|
||||||
|
* @param int $code only HTTP 3XX codes are accepted.
|
||||||
|
* @return HttpResponse a response that will redirect client to given url
|
||||||
|
*/
|
||||||
|
public static function redirect(string $url, int $code = HttpCodes::FOUND): HttpResponse {
|
||||||
|
if ($code < 300 || $code >= 400) {
|
||||||
|
throw new \InvalidArgumentException("given code is not a redirection http code");
|
||||||
|
}
|
||||||
|
return new HttpResponse($code, ["Location" => $url]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||||||||
|
<?php
|
||||||||||||
|
|
||||||||||||
|
namespace IQBall\Core\Model;
|
||||||||||||
|
|
||||||||||||
|
use IQBall\Core\Data\Account;
|
||||||||||||
|
use IQBall\Core\Gateway\AccountGateway;
|
||||||||||||
|
use IQBall\Core\Validation\FieldValidationFail;
|
||||||||||||
|
use IQBall\Core\Validation\ValidationFail;
|
||||||||||||
|
|
||||||||||||
|
class AuthModel {
|
||||||||||||
|
private AccountGateway $gateway;
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param AccountGateway $gateway
|
||||||||||||
|
*/
|
||||||||||||
|
public function __construct(AccountGateway $gateway) {
|
||||||||||||
|
$this->gateway = $gateway;
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param string $username
|
||||||||||||
|
* @param string $password
|
||||||||||||
|
* @param string $confirmPassword
|
||||||||||||
|
* @param string $email
|
||||||||||||
|
* @param ValidationFail[] $failures
|
||||||||||||
|
* @return Account|null the registered account or null if failures occurred
|
||||||||||||
|
*/
|
||||||||||||
|
public function register(string $username, string $password, string $confirmPassword, string $email, array &$failures): ?Account {
|
||||||||||||
maxime.batista marked this conversation as resolved
|
|||||||||||||
|
|
||||||||||||
|
if ($password != $confirmPassword) {
|
||||||||||||
|
$failures[] = new FieldValidationFail("confirmpassword", "Le mot de passe et la confirmation ne sont pas les mêmes.");
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
if ($this->gateway->exists($email)) {
|
||||||||||||
|
$failures[] = new FieldValidationFail("email", "L'email existe déjà");
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
if (!empty($failures)) {
|
||||||||||||
|
return null;
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||||||||||
|
|
||||||||||||
|
$token = $this->generateToken();
|
||||||||||||
|
$accountId = $this->gateway->insertAccount($username, $email, $token, $hash);
|
||||||||||||
|
return new Account($email, $username, $token, $accountId);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* Generate a random base 64 string
|
||||||||||||
|
* @return string
|
||||||||||||
|
*/
|
||||||||||||
|
private function generateToken(): string {
|
||||||||||||
|
return base64_encode(random_bytes(64));
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* @param string $email
|
||||||||||||
|
* @param string $password
|
||||||||||||
|
* @param ValidationFail[] $failures
|
||||||||||||
|
* @return Account|null the authenticated account or null if failures occurred
|
||||||||||||
|
*/
|
||||||||||||
|
public function login(string $email, string $password, array &$failures): ?Account {
|
||||||||||||
|
$hash = $this->gateway->getHash($email);
|
||||||||||||
|
if ($hash == null) {
|
||||||||||||
|
$failures[] = new FieldValidationFail("email", "l'addresse email n'est pas connue.");
|
||||||||||||
|
return null;
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
if (!password_verify($password, $hash)) {
|
||||||||||||
|
$failures[] = new FieldValidationFail("password", "Mot de passe invalide.");
|
||||||||||||
|
return null;
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return $this->gateway->getAccountFromMail($email);
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\Core\Model;
|
||||||
|
|
||||||
|
use IQBall\Core\Gateway\TacticInfoGateway;
|
||||||
|
use IQBall\Core\Validation\ValidationFail;
|
||||||
|
use IQBall\Core\Data\TacticInfo;
|
||||||
|
|
||||||
|
class TacticModel {
|
||||||
|
public const TACTIC_DEFAULT_NAME = "Nouvelle tactique";
|
||||||
|
|
||||||
|
|
||||||
|
private TacticInfoGateway $tactics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TacticInfoGateway $tactics
|
||||||
|
*/
|
||||||
|
public function __construct(TacticInfoGateway $tactics) {
|
||||||
|
$this->tactics = $tactics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates a new empty tactic, with given name
|
||||||
|
* @param string $name
|
||||||
|
* @param int $ownerId
|
||||||
|
* @return TacticInfo
|
||||||
|
*/
|
||||||
|
public function makeNew(string $name, int $ownerId): TacticInfo {
|
||||||
|
return $this->tactics->insert($name, $ownerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates a new empty tactic, with a default name
|
||||||
|
* @param int $ownerId
|
||||||
|
* @return TacticInfo|null
|
||||||
|
*/
|
||||||
|
public function makeNewDefault(int $ownerId): ?TacticInfo {
|
||||||
|
return $this->makeNew(self::TACTIC_DEFAULT_NAME, $ownerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to retrieve information about a tactic
|
||||||
|
* @param int $id tactic identifier
|
||||||
|
* @return TacticInfo|null or null if the identifier did not match a tactic
|
||||||
|
*/
|
||||||
|
public function get(int $id): ?TacticInfo {
|
||||||
|
return $this->tactics->get($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the nb last tactics created
|
||||||
|
*
|
||||||
|
* @param integer $nb
|
||||||
|
* @return array<array<string,mixed>>
|
||||||
|
*/
|
||||||
|
public function getLast(int $nb): ?array {
|
||||||
|
return $this->tactics->getLast($nb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the name of a tactic
|
||||||
|
* @param int $id the tactic identifier
|
||||||
|
* @param string $name the new name to set
|
||||||
|
* @return ValidationFail[] failures, if any
|
||||||
|
*/
|
||||||
|
public function updateName(int $id, string $name, int $authId): array {
|
||||||
|
|
||||||
|
$tactic = $this->tactics->get($id);
|
||||||
|
|
||||||
|
if ($tactic == null) {
|
||||||
|
return [ValidationFail::notFound("Could not find tactic")];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tactic->getOwnerId() != $authId) {
|
||||||
|
return [ValidationFail::unauthorized()];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->tactics->updateName($id, $name);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\Core\Model;
|
||||||
|
|
||||||
|
use IQBall\Core\Data\Color;
|
||||||
|
use IQBall\Core\Data\Team;
|
||||||
|
use IQBall\Core\Data\TeamInfo;
|
||||||
|
use IQBall\Core\Gateway\AccountGateway;
|
||||||
|
use IQBall\Core\Gateway\MemberGateway;
|
||||||
|
use IQBall\Core\Gateway\TeamGateway;
|
||||||
|
|
||||||
|
class TeamModel {
|
||||||
|
private AccountGateway $users;
|
||||||
|
private TeamGateway $teams;
|
||||||
|
private MemberGateway $members;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TeamGateway $gateway
|
||||||
|
* @param MemberGateway $members
|
||||||
|
* @param AccountGateway $users
|
||||||
|
*/
|
||||||
|
public function __construct(TeamGateway $gateway, MemberGateway $members, AccountGateway $users) {
|
||||||
|
$this->teams = $gateway;
|
||||||
|
$this->members = $members;
|
||||||
|
$this->users = $users;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @param string $picture
|
||||||
|
* @param string $mainColor
|
||||||
|
* @param string $secondColor
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function createTeam(string $name, string $picture, string $mainColor, string $secondColor): int {
|
||||||
|
return $this->teams->insert($name, $picture, $mainColor, $secondColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* adds a member to a team
|
||||||
|
* @param string $mail
|
||||||
|
* @param int $teamId
|
||||||
|
* @param string $role
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addMember(string $mail, int $teamId, string $role): void {
|
||||||
|
$userId = $this->users->getAccountFromMail($mail)->getId();
|
||||||
|
$this->members->insert($teamId, $userId, $role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @return TeamInfo[]
|
||||||
|
*/
|
||||||
|
public function listByName(string $name): array {
|
||||||
|
return $this->teams->listByName($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $id
|
||||||
|
* @return Team
|
||||||
|
*/
|
||||||
|
public function getTeam(int $id): Team {
|
||||||
|
$teamInfo = $this->teams->getTeamById($id);
|
||||||
|
$members = $this->members->getMembersOfTeam($id);
|
||||||
|
return new Team($teamInfo, $members);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* delete a member from given team identifier
|
||||||
|
* @param string $mail
|
||||||
|
* @param int $teamId
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function deleteMember(string $mail, int $teamId): int {
|
||||||
|
$userId = $this->users->getAccountFromMail($mail)->getId();
|
||||||
|
$this->members->remove($teamId, $userId);
|
||||||
|
return $teamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\Core\Session;
|
||||||
|
|
||||||
|
use IQBall\Core\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 IQBall\Core\Session;
|
||||||
|
|
||||||
|
use IQBall\Core\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 IQBall\Core\Session;
|
||||||
|
|
||||||
|
use IQBall\Core\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;
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Validation;
|
namespace IQBall\Core\Validation;
|
||||||
|
|
||||||
class ComposedValidator extends Validator {
|
class ComposedValidator extends Validator {
|
||||||
private Validator $first;
|
private Validator $first;
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Validation;
|
namespace IQBall\Core\Validation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An error that concerns a field, with a bound message name
|
* An error that concerns a field, with a bound message name
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Validation;
|
namespace IQBall\Core\Validation;
|
||||||
|
|
||||||
class FunctionValidator extends Validator {
|
class FunctionValidator extends Validator {
|
||||||
/**
|
/**
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Validation;
|
namespace IQBall\Core\Validation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple validator that takes a predicate and an error factory
|
* A simple validator that takes a predicate and an error factory
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Validation;
|
namespace IQBall\Core\Validation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class for validation
|
* Utility class for validation
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Validation;
|
namespace IQBall\Core\Validation;
|
||||||
|
|
||||||
abstract class Validator {
|
abstract class Validator {
|
||||||
/**
|
/**
|
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IQBall\Core\Validator;
|
||||||
|
|
||||||
|
use IQBall\Core\Data\TacticInfo;
|
||||||
|
use IQBall\Core\Validation\ValidationFail;
|
||||||
|
|
||||||
|
class TacticValidator {
|
||||||
|
/**
|
||||||
|
* @param TacticInfo|null $tactic
|
||||||
|
* @param int $tacticId
|
||||||
|
* @param int $ownerId
|
||||||
|
* @return ValidationFail|null
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,105 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
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
|
|
||||||
* to share to other users, or non-needed public information
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
private AccountUser $user;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Team[] account's teams
|
|
||||||
*/
|
|
||||||
private array $teams;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var int account's unique identifier
|
|
||||||
*/
|
|
||||||
private int $id;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $email
|
|
||||||
* @param string $phoneNumber
|
|
||||||
* @param AccountUser $user
|
|
||||||
* @param Team[] $teams
|
|
||||||
* @param int $id
|
|
||||||
*/
|
|
||||||
public function __construct(string $email, string $phoneNumber, AccountUser $user, array $teams, int $id) {
|
|
||||||
$this->email = $email;
|
|
||||||
$this->phoneNumber = $phoneNumber;
|
|
||||||
$this->user = $user;
|
|
||||||
$this->teams = $teams;
|
|
||||||
$this->id = $id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getEmail(): string {
|
|
||||||
return $this->email;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $email
|
|
||||||
*/
|
|
||||||
public function setEmail(string $email): void {
|
|
||||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
||||||
throw new InvalidArgumentException("Invalid mail address");
|
|
||||||
}
|
|
||||||
$this->email = $email;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getPhoneNumber(): string {
|
|
||||||
return $this->phoneNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $phoneNumber
|
|
||||||
*/
|
|
||||||
public function setPhoneNumber(string $phoneNumber): void {
|
|
||||||
if (!preg_match(PHONE_NUMBER_REGEXP, $phoneNumber)) {
|
|
||||||
throw new InvalidArgumentException("Invalid phone number");
|
|
||||||
}
|
|
||||||
$this->phoneNumber = $phoneNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getId(): int {
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Team[]
|
|
||||||
*/
|
|
||||||
public function getTeams(): array {
|
|
||||||
return $this->teams;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUser(): AccountUser {
|
|
||||||
return $this->user;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Data;
|
|
||||||
|
|
||||||
use http\Url;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class implements the User and
|
|
||||||
*/
|
|
||||||
class AccountUser implements User {
|
|
||||||
private string $name;
|
|
||||||
private Url $profilePicture;
|
|
||||||
private int $age;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @param Url $profilePicture
|
|
||||||
* @param int $age
|
|
||||||
*/
|
|
||||||
public function __construct(string $name, Url $profilePicture, int $age) {
|
|
||||||
$this->name = $name;
|
|
||||||
$this->profilePicture = $profilePicture;
|
|
||||||
$this->age = $age;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function getName(): string {
|
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getProfilePicture(): Url {
|
|
||||||
return $this->profilePicture;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAge(): int {
|
|
||||||
return $this->age;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setName(string $name): void {
|
|
||||||
$this->name = $name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setProfilePicture(Url $profilePicture): void {
|
|
||||||
$this->profilePicture = $profilePicture;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setAge(int $age): void {
|
|
||||||
$this->age = $age;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Data;
|
|
||||||
|
|
||||||
use http\Url;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Public information about a user
|
|
||||||
*/
|
|
||||||
interface User {
|
|
||||||
/**
|
|
||||||
* @return string the user's name
|
|
||||||
*/
|
|
||||||
public function getName(): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Url The user's profile picture image URL
|
|
||||||
*/
|
|
||||||
|
|
||||||
public function getProfilePicture(): Url;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int The user's age
|
|
||||||
*/
|
|
||||||
public function getAge(): int;
|
|
||||||
}
|
|
@ -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", []);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Gateway;
|
|
||||||
|
|
||||||
use App\Connexion;
|
|
||||||
use App\Data\TacticInfo;
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
class TacticInfoGateway {
|
|
||||||
private Connexion $con;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Connexion $con
|
|
||||||
*/
|
|
||||||
public function __construct(Connexion $con) {
|
|
||||||
$this->con = $con;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get(int $id): ?TacticInfo {
|
|
||||||
$res = $this->con->fetch(
|
|
||||||
"SELECT * FROM TacticInfo WHERE id = :id",
|
|
||||||
[":id" => [$id, PDO::PARAM_INT]]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isset($res[0])) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$row = $res[0];
|
|
||||||
|
|
||||||
return new TacticInfo($id, $row["name"], strtotime($row["creation_date"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function insert(string $name): TacticInfo {
|
|
||||||
$this->con->exec(
|
|
||||||
"INSERT INTO TacticInfo(name) VALUES(:name)",
|
|
||||||
[":name" => [$name, PDO::PARAM_STR]]
|
|
||||||
);
|
|
||||||
$row = $this->con->fetch(
|
|
||||||
"SELECT id, creation_date FROM TacticInfo WHERE :id = id",
|
|
||||||
[':id' => [$this->con->lastInsertId(), PDO::PARAM_INT]]
|
|
||||||
)[0];
|
|
||||||
return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateName(int $id, string $name): void {
|
|
||||||
$this->con->exec(
|
|
||||||
"UPDATE TacticInfo SET name = :name WHERE id = :id",
|
|
||||||
[
|
|
||||||
":name" => [$name, PDO::PARAM_STR],
|
|
||||||
":id" => [$id, PDO::PARAM_INT],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Gateway;
|
|
||||||
|
|
||||||
use App\Connexion;
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
class TeamGateway {
|
|
||||||
private Connexion $con;
|
|
||||||
|
|
||||||
public function __construct(Connexion $con) {
|
|
||||||
$this->con = $con;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function insert(string $name, string $picture, string $mainColor, string $secondColor): void {
|
|
||||||
$this->con->exec(
|
|
||||||
"INSERT INTO Team(name, picture, mainColor, secondColor) VALUES (:teamName , :picture, :mainColor, :secondColor)",
|
|
||||||
[
|
|
||||||
":teamName" => [$name, PDO::PARAM_STR],
|
|
||||||
":picture" => [$picture, PDO::PARAM_STR],
|
|
||||||
":mainColor" => [$mainColor, PDO::PARAM_STR],
|
|
||||||
":secondColor" => [$secondColor, PDO::PARAM_STR],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @return array<string,mixed>[]
|
|
||||||
*/
|
|
||||||
public function listByName(string $name): array {
|
|
||||||
return $this->con->fetch(
|
|
||||||
"SELECT id,name,picture,mainColor,secondColor FROM Team WHERE name LIKE '%' || :name || '%'",
|
|
||||||
[
|
|
||||||
":name" => [$name, PDO::PARAM_STR],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $id
|
|
||||||
* @return array<string,mixed>[]
|
|
||||||
*/
|
|
||||||
public function getTeamById(int $id): array {
|
|
||||||
return $this->con->fetch(
|
|
||||||
"SELECT id,name,picture,mainColor,secondColor FROM Team WHERE id = :id",
|
|
||||||
[
|
|
||||||
":id" => [$id, PDO::PARAM_INT],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @return array<string,int>[]
|
|
||||||
*/
|
|
||||||
public function getIdTeamByName(string $name): array {
|
|
||||||
return $this->con->fetch(
|
|
||||||
"SELECT id FROM Team WHERE name = :name",
|
|
||||||
[
|
|
||||||
":name" => [$name, PDO::PARAM_STR],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $id
|
|
||||||
* @return array<string,mixed>[]
|
|
||||||
*/
|
|
||||||
public function getMembersById(int $id): array {
|
|
||||||
return $this->con->fetch(
|
|
||||||
"SELECT m.role,u.id FROM User u,Team t,Member m WHERE t.id = :id AND m.idTeam = t.id AND m.idMember = u.id",
|
|
||||||
[
|
|
||||||
":id" => [$id, PDO::PARAM_INT],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http;
|
|
||||||
|
|
||||||
class HttpResponse {
|
|
||||||
private int $code;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $code
|
|
||||||
*/
|
|
||||||
public function __construct(int $code) {
|
|
||||||
$this->code = $code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCode(): int {
|
|
||||||
return $this->code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromCode(int $code): HttpResponse {
|
|
||||||
return new HttpResponse($code);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Model;
|
|
||||||
|
|
||||||
use App\Controller\AuthController;
|
|
||||||
use App\Gateway\AuthGateway;
|
|
||||||
use App\Validation\FieldValidationFail;
|
|
||||||
use App\Validation\ValidationFail;
|
|
||||||
|
|
||||||
class AuthModel {
|
|
||||||
private AuthGateway $gateway;
|
|
||||||
/**
|
|
||||||
* @param AuthGateway $gateway
|
|
||||||
*/
|
|
||||||
public function __construct(AuthGateway $gateway) {
|
|
||||||
$this->gateway = $gateway;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $username
|
|
||||||
* @param string $password
|
|
||||||
* @param string $confirmPassword
|
|
||||||
* @param string $email
|
|
||||||
* @return ValidationFail[]
|
|
||||||
*/
|
|
||||||
public function register(string $username, string $password, string $confirmPassword, string $email): array {
|
|
||||||
$errors = [];
|
|
||||||
|
|
||||||
if ($password != $confirmPassword) {
|
|
||||||
$errors[] = new FieldValidationFail("confirmpassword", "password and password confirmation are not equals");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->gateway->mailExist($email)) {
|
|
||||||
$errors[] = new FieldValidationFail("email", "email already exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(empty($errors)) {
|
|
||||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
|
||||||
$this->gateway->insertAccount($username, $hash, $email);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $email
|
|
||||||
* @return array<string,string>|null
|
|
||||||
*/
|
|
||||||
public function getUserFields(string $email): ?array {
|
|
||||||
return $this->gateway->getUserFields($email);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $email
|
|
||||||
* @param string $password
|
|
||||||
* @return ValidationFail[] $errors
|
|
||||||
*/
|
|
||||||
public function login(string $email, string $password): array {
|
|
||||||
$errors = [];
|
|
||||||
|
|
||||||
if (!$this->gateway->mailExist($email)) {
|
|
||||||
$errors[] = new FieldValidationFail("email", "email doesnt exists");
|
|
||||||
return $errors;
|
|
||||||
}
|
|
||||||
$hash = $this->gateway->getUserHash($email);
|
|
||||||
|
|
||||||
if (!password_verify($password, $hash)) {
|
|
||||||
$errors[] = new FieldValidationFail("password", "invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Model;
|
|
||||||
|
|
||||||
use App\Data\TacticInfo;
|
|
||||||
use App\Gateway\TacticInfoGateway;
|
|
||||||
|
|
||||||
class TacticModel {
|
|
||||||
public const TACTIC_DEFAULT_NAME = "Nouvelle tactique";
|
|
||||||
|
|
||||||
|
|
||||||
private TacticInfoGateway $tactics;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param TacticInfoGateway $tactics
|
|
||||||
*/
|
|
||||||
public function __construct(TacticInfoGateway $tactics) {
|
|
||||||
$this->tactics = $tactics;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function makeNew(string $name): TacticInfo {
|
|
||||||
return $this->tactics->insert($name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function makeNewDefault(): ?TacticInfo {
|
|
||||||
return $this->tactics->insert(self::TACTIC_DEFAULT_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tries to retrieve information about a tactic
|
|
||||||
* @param int $id tactic identifier
|
|
||||||
* @return TacticInfo|null or null if the identifier did not match a tactic
|
|
||||||
*/
|
|
||||||
public function get(int $id): ?TacticInfo {
|
|
||||||
return $this->tactics->get($id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the name of a tactic
|
|
||||||
* @param int $id the tactic identifier
|
|
||||||
* @param string $name the new name to set
|
|
||||||
* @return bool true if the update was done successfully
|
|
||||||
*/
|
|
||||||
public function updateName(int $id, string $name): bool {
|
|
||||||
if ($this->tactics->get($id) == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->tactics->updateName($id, $name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Model;
|
|
||||||
|
|
||||||
use App\Gateway\TeamGateway;
|
|
||||||
use App\Data\Team;
|
|
||||||
use App\Data\Member;
|
|
||||||
use App\Data\MemberRole;
|
|
||||||
use App\Data\Color;
|
|
||||||
|
|
||||||
class TeamModel {
|
|
||||||
private TeamGateway $gateway;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param TeamGateway $gateway
|
|
||||||
*/
|
|
||||||
public function __construct(TeamGateway $gateway) {
|
|
||||||
$this->gateway = $gateway;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createTeam(string $name, string $picture, string $mainColor, string $secondColor): int {
|
|
||||||
$this->gateway->insert($name, $picture, $mainColor, $secondColor);
|
|
||||||
$result = $this->gateway->getIdTeamByName($name);
|
|
||||||
return intval($result[0]['id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @return Team[]
|
|
||||||
*/
|
|
||||||
public function listByName(string $name): array {
|
|
||||||
$teams = [];
|
|
||||||
$results = $this->gateway->listByName($name);
|
|
||||||
foreach ($results as $row) {
|
|
||||||
$teams[] = new Team($row['id'], $row['name'], $row['picture'], Color::from($row['mainColor']), Color::from($row['secondColor']));
|
|
||||||
}
|
|
||||||
return $teams;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function displayTeam(int $id): Team {
|
|
||||||
$members = [];
|
|
||||||
$result = $this->gateway->getTeamById($id)[0];
|
|
||||||
$resultMembers = $this->gateway->getMembersById($id);
|
|
||||||
foreach ($resultMembers as $row) {
|
|
||||||
if ($row['role'] == 'C') {
|
|
||||||
$role = MemberRole::coach();
|
|
||||||
} else {
|
|
||||||
$role = MemberRole::player();
|
|
||||||
}
|
|
||||||
$members[] = new Member($row['id'], $role);
|
|
||||||
}
|
|
||||||
return new Team(intval($result['id']), $result['name'], $result['picture'], Color::from($result['mainColor']), Color::from($result['secondColor']), $members);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Twig view</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>Hello, this is a sample form made in Twig !</h1>
|
|
||||||
|
|
||||||
<form action="submit-twig" method="POST">
|
|
||||||
<label for="name">your name: </label>
|
|
||||||
<input type="text" id="name" name="name"/>
|
|
||||||
<label for="password">a little description about yourself: </label>
|
|
||||||
<input type="text" id="password" name="description"/>
|
|
||||||
<input type="submit" value="click me to submit!"/>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Loading…
Reference in new issue
needs some documentation here plz