Merge Wednesday Rush and remove Salva's imposed conception aberrations #22

Merged
maxime.batista merged 48 commits from salva into master 1 year ago

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

@ -28,7 +28,7 @@ If we take a look at the request, we'll see that the url does not targets `local
`localhost:5173` is the react development server, it is able to serve our react front view files. `localhost:5173` is the react development server, it is able to serve our react front view files.
Let's run the react development server. Let's run the react development server.
It is a simple as running `npm start` in a new terminal (be sure to run it in the repository's directory). It is as simple as running `npm start` in a new terminal (be sure to run it in the repository's directory).
![](assets/npm-start.png) ![](assets/npm-start.png)
You should see something like this, it says that the server was opened on port `5173`, thats our react development server ! You should see something like this, it says that the server was opened on port `5173`, thats our react development server !
@ -40,7 +40,7 @@ Caution: **NEVER** directly connect on the `localhost:5173` node development ser
# How it works # How it works
I'm glad you are interested in how that stuff works, it's a bit tricky, lets go. I'm glad you are interested in how that stuff works, it's a bit tricky, lets go.
If you look at our `index.php` (located in `/public` folder), you'll see that it is our gateway, it uses an `AltoRouter` that dispatches the request's process to a controller. If you look at our `index.php` (located in `/public` folder), you'll see that it define our routes, it uses an `AltoRouter` then delegates the request's action processing to a controller.
We can see that there are two registered routes, the `GET:/` (that then calls `SampleFormController#displayForm()`) and `POST:/result` (that calls `SampleFormController#displayResults()`). We can see that there are two registered routes, the `GET:/` (that then calls `SampleFormController#displayForm()`) and `POST:/result` (that calls `SampleFormController#displayResults()`).
Implementation of those two methods are very simple: there is no verification to make nor model to control, thus they directly sends the view back to the client. Implementation of those two methods are very simple: there is no verification to make nor model to control, thus they directly sends the view back to the client.
@ -115,7 +115,7 @@ function _asset(string $assetURI): string {
The simplest profile, simply redirect all assets to the development server The simplest profile, simply redirect all assets to the development server
### Production profile ### Production profile
Before the CD deploys the generated files to the server, Before the CD workflow step deploys the generated files to the server,
it generates a `/views-mappings.php` file that will map the react views file names to their compiled javascript files : it generates a `/views-mappings.php` file that will map the react views file names to their compiled javascript files :
```php ```php
@ -138,12 +138,3 @@ function _asset(string $assetURI): string {
return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI); return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI);
} }
``` ```
## React views conventions.
Conventions regarding our react views __must be respected in order to be renderable__.
### The `render(any)` function
Any React view component needs to be default exported in order to be imported and used from PHP. Those components will receive as props the arguments that the PHP server has transmitted.
The `arguments` parameter is used to pass data to the react component.
If you take a look at the `front/views/SampleForm.tsx` view, here's the definition of its render function :

@ -35,7 +35,7 @@ class ViewHttpResponse extends HttpResponse {
- arguments: array - arguments: array
- kind: int - kind: int
+ __construct(kind: int, file: string, arguments: array, code: int = HttpCodes::OK) - __construct(kind: int, file: string, arguments: array, code: int = HttpCodes::OK)
+ getViewKind(): int + getViewKind(): int
+ getFile(): string + getFile(): string
+ getArguments(): array + getArguments(): array
@ -44,4 +44,8 @@ class ViewHttpResponse extends HttpResponse {
+ <u>react(file: string, arguments: array, code: int = HttpCodes::OK): ViewHttpResponse + <u>react(file: string, arguments: array, code: int = HttpCodes::OK): ViewHttpResponse
} }
note right of ViewHttpResponse
Into src/App
end note
@enduml @enduml

@ -1,42 +1,36 @@
@startuml @startuml
class Account { class TacticInfo {
- email: String
- phoneNumber: String
- id: int - id: int
- name: string
- creationDate: string
- ownerId: string
+ setMailAddress(String)
+ getMailAddress(): String
+ getPhoneNumber(): String
+ setPhoneNumber(String)
+ getUser(): AccountUser
+ getId(): int + getId(): int
+ getOwnerId(): int
+ getCreationTimestamp(): int
+ getName(): string
} }
Account --> "- user" AccountUser class Account {
Account --> "- teams *" Team - email: string
- token: string
interface User { - name: string
+ getName(): String - id: int
+ getProfilePicture(): Url
+ getAge(): int
}
class AccountUser {
- name: String
- profilePicture: Url
- age: int
+ setName(String) + getMailAddress(): string
+ setProfilePicture(URI) + getToken(): string
+ setAge(int) + getName(): string
+ getId(): int
} }
AccountUser ..|> User
class Member { class Member {
- userId: int - userId: int
- teamId: int
+ __construct(role : MemberRole)
+ getUserId(): int + getUserId(): int
+ getTeamId(): int
+ getRole(): MemberRole + getRole(): MemberRole
} }
@ -47,19 +41,27 @@ enum MemberRole {
COACH COACH
} }
class Team {
- name: String
- picture: Url
+ getName(): String class TeamInfo {
+ getPicture(): Url - creationDate: int
- name: string
- picture: string
+ getName(): string
+ getPicture(): string
+ getMainColor(): Color + getMainColor(): Color
+ getSecondColor(): Color + getSecondColor(): Color
+ listMembers(): array<Member>
} }
Team --> "- mainColor" Color TeamInfo --> "- mainColor" Color
Team --> "- secondaryColor" Color TeamInfo --> "- secondaryColor" Color
class Team {
getInfo(): TeamInfo
listMembers(): Member[]
}
Team --> "- info" TeamInfo
Team --> "- members *" Member Team --> "- members *" Member
class Color { class Color {
@ -68,30 +70,4 @@ class Color {
+ getValue(): int + getValue(): int
} }
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
+ getUserFields(email : string):array
+ login(email : string, password : string)
}
AuthModel --> "- gateway" AuthGateway
class AuthGateway{
-con : Connection
+ mailExist(email : string) : bool
+ insertAccount(username : string, hash : string, email : string)
+ getUserHash(email : string):string
+ getUserFields (email : string): array
}
@enduml @enduml

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

@ -1,10 +1,11 @@
@startuml @startuml
class Team { class Team {
- name: String - name: string
- picture: Url - picture: Url
- members: array<int, MemberRole> - members: array<int, MemberRole>
+ getName(): String + __construct(name : string, picture : string, mainColor : Colo, secondColor : Color)
+ getName(): string
+ getPicture(): Url + getPicture(): Url
+ getMainColor(): Color + getMainColor(): Color
+ getSecondColor(): Color + getSecondColor(): Color
@ -57,12 +58,6 @@ class TeamController{
TeamController *--"- model" TeamModel TeamController *--"- model" TeamModel
class Connexion{ class Connexion { }
- pdo : PDO
--
+ __constructor(pdo : PDO)
+ exec(query : string, args : array)
+ fetch(query string, args array): array
}
@enduml @enduml

@ -50,9 +50,11 @@ class Validation {
} }
class Validators { class Validators {
---
+ <u>nonEmpty(): Validator + <u>nonEmpty(): Validator
+ <u>shorterThan(limit: int): Validator + <u>shorterThan(limit: int): Validator
+ <u>userString(maxLen: int): Validator + <u>userString(maxLen: int): Validator
...
} }

@ -1,7 +1,7 @@
{ {
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"App\\": "src/" "IQBall\\": "src/"
} }
}, },
"require": { "require": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M234-276q51-39 114-61.5T480-360q69 0 132 22.5T726-276q35-41 54.5-93T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 59 19.5 111t54.5 93Zm246-164q-59 0-99.5-40.5T340-580q0-59 40.5-99.5T480-720q59 0 99.5 40.5T620-580q0 59-40.5 99.5T480-440Zm0 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q53 0 100-15.5t86-44.5q-39-29-86-44.5T480-280q-53 0-100 15.5T294-220q39 29 86 44.5T480-160Zm0-360q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm0-60Zm0 360Z"/></svg>

After

Width:  |  Height:  |  Size: 732 B

@ -3,11 +3,10 @@ parameters:
level: 6 level: 6
paths: paths:
- src - src
- public
scanFiles: scanFiles:
- config.php - config.php
- sql/database.php - sql/database.php
- profiles/dev-config-profile.php - profiles/dev-config-profile.php
- profiles/prod-config-profile.php - profiles/prod-config-profile.php
excludePaths: excludePaths:
- src/react-display-file.php - src/App/react-display-file.php

@ -5,38 +5,33 @@ require "../../vendor/autoload.php";
require "../../sql/database.php"; require "../../sql/database.php";
require "../utils.php"; require "../utils.php";
use App\Connexion; use IQBall\Api\API;
use App\Controller\Api\APITacticController; use IQBall\Api\Controller\APIAuthController;
use App\Gateway\TacticInfoGateway; use IQBall\Api\Controller\APITacticController;
use App\Http\JsonHttpResponse; use IQBall\Core\Action;
use App\Http\ViewHttpResponse; use IQBall\Core\Connection;
use App\Model\TacticModel; use IQBall\Core\Data\Account;
use IQBall\Core\Gateway\AccountGateway;
$con = new Connexion(get_database()); use IQBall\Core\Gateway\TacticInfoGateway;
use IQBall\Core\Model\AuthModel;
$router = new AltoRouter(); use IQBall\Core\Model\TacticModel;
$router->setBasePath(get_public_path() . "/api");
function getTacticController(): APITacticController {
$tacticEndpoint = new APITacticController(new TacticModel(new TacticInfoGateway($con))); return new APITacticController(new TacticModel(new TacticInfoGateway(new Connection(get_database()))));
$router->map("POST", "/tactic/[i:id]/edit/name", fn(int $id) => $tacticEndpoint->updateName($id)); }
$router->map("GET", "/tactic/[i:id]", fn(int $id) => $tacticEndpoint->getTacticInfo($id));
$router->map("POST", "/tactic/new", fn() => $tacticEndpoint->newTactic());
$match = $router->match();
if ($match == null) { function getAuthController(): APIAuthController {
echo "404 not found"; return new APIAuthController(new AuthModel(new AccountGateway(new Connection(get_database()))));
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
exit(1);
} }
$response = call_user_func_array($match['target'], $match['params']); function getRoutes(): AltoRouter {
$router = new AltoRouter();
$router->setBasePath(get_public_path() . "/api");
http_response_code($response->getCode()); $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc)));
$router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize()));
if ($response instanceof JsonHttpResponse) { return $router;
header('Content-type: application/json');
echo $response->getJson();
} elseif ($response instanceof ViewHttpResponse) {
throw new Exception("API returned a view http response.");
} }
Api::render(API::handleMatch(getRoutes()->match()));

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

needs some documentation here plz

needs some documentation here plz
/**
* 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
Review

Note that the router can generate URIs, that already take care of the base path.

Note that the router can generate URIs, that already take care of the base path.
}
/**
* 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
Review
-                'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")], 
+                'failures' => [ValidationFail::notFound("Could not find page {$_SERVER['REQUEST_URI']}.")], 

It is deprecated since PHP 8.2.

```diff - 'failures' => [ValidationFail::notFound("Could not find page ${_SERVER['REQUEST_URI']}.")], + 'failures' => [ValidationFail::notFound("Could not find page {$_SERVER['REQUEST_URI']}.")], ``` It is deprecated since [PHP 8.2](https://wiki.php.net/rfc/deprecate_dollar_brace_string_interpolation).
// 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);
}
}

@ -1,14 +1,14 @@
<?php <?php
namespace App\Controller; namespace IQBall\Core\Route;
use App\Http\HttpCodes; use IQBall\App\ViewHttpResponse;
use App\Http\HttpRequest; use IQBall\Core\Http\HttpCodes;
use App\Http\HttpResponse; use IQBall\Core\Http\HttpRequest;
use App\Http\JsonHttpResponse; use IQBall\Core\Http\HttpResponse;
use App\Http\ViewHttpResponse; use IQBall\Core\Http\JsonHttpResponse;
use App\Validation\ValidationFail; use IQBall\Core\Validation\ValidationFail;
use App\Validation\Validator; use IQBall\Core\Validation\Validator;
class Control { class Control {
/** /**
@ -57,6 +57,4 @@ class Control {
return call_user_func_array($run, [$request]); return call_user_func_array($run, [$request]);
} }
} }

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

This is the third time this regex is repeated. Consider using filter_var and defining this elsewhere.

This is the third time this regex is repeated. Consider using `filter_var` and defining this elsewhere.
* 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()]);
}
}

@ -1,6 +1,9 @@
<?php <?php
namespace App\Http; namespace IQBall\App;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpResponse;
class ViewHttpResponse extends HttpResponse { class ViewHttpResponse extends HttpResponse {
public const TWIG_VIEW = 0; public const TWIG_VIEW = 0;
@ -26,7 +29,7 @@ class ViewHttpResponse extends HttpResponse {
* @param array<string, mixed> $arguments * @param array<string, mixed> $arguments
*/ */
private function __construct(int $kind, string $file, array $arguments, int $code = HttpCodes::OK) { private function __construct(int $kind, string $file, array $arguments, int $code = HttpCodes::OK) {
parent::__construct($code); parent::__construct($code, []);
$this->kind = $kind; $this->kind = $kind;
$this->file = $file; $this->file = $file;
$this->arguments = $arguments; $this->arguments = $arguments;

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

@ -53,26 +53,32 @@
background-color: #0056b3; background-color: #0056b3;
} }
{% for err in bad_fields %} .error-messages{
.form-group #{{ err }} { color : #ff331a;
font-style: italic;
}
{% for err in fails %}
.form-group #{{ err.getFieldName() }} {
border-color: red; border-color: red;
} }
{% endfor %} {% endfor %}
</style> </style>
<div class="container"> <div class="container">
<center><h2>Se connecter</h2></center> <center><h2>Se connecter</h2></center>
<form action="login" method="post"> <form action="login" method="post">
<div class="form-group"> <div class="form-group">
{% for name in fails %}
<label class="error-messages"> {{ name.getFieldName() }} : {{ name.getMessage()}} </label>
{% endfor %}
<label for="email">Email :</label> <label for="email">Email :</label>
<input type="text" id="email" name="email" required> <input type="text" id="email" name="email" required>
<label for= "password">Mot de passe :</label> <label for= "password">Mot de passe :</label>
<input type="password" id="password" name="password" required> <input type="password" id="password" name="password" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="submit" value="S'identifier"> <input type="submit" value="S'identifier">

@ -49,12 +49,17 @@
cursor: pointer; cursor: pointer;
} }
.error-messages{
color : #ff331a;
font-style: italic;
}
input[type="submit"]:hover { input[type="submit"]:hover {
background-color: #0056b3; background-color: #0056b3;
} }
{% for err in bad_fields %} {% for err in fails %}
.form-group #{{ err }} { .form-group #{{ err.getFieldName() }} {
border-color: red; border-color: red;
} }
{% endfor %} {% endfor %}
@ -67,6 +72,11 @@
<center><h2>S'enregistrer</h2></center> <center><h2>S'enregistrer</h2></center>
<form action="register" method="post"> <form action="register" method="post">
<div class="form-group"> <div class="form-group">
{% for name in fails %}
<label class = "error-messages"> {{ name.getFieldName() }} : {{ name.getMessage() }} </label>
{% endfor %}
<label for="username">Nom d'utilisateur :</label> <label for="username">Nom d'utilisateur :</label>
<input type="text" id="username" name="username" required> <input type="text" id="username" name="username" required>
<label for= "password">Mot de passe :</label> <label for= "password">Mot de passe :</label>

@ -20,17 +20,13 @@
height:50px; height:50px;
} }
#mainColor{ #main_color {
background-color: {{ team.mainColor.getValue() }}; border: solid;
{% if team.mainColor.getValue() == "#ffffff" %} background-color: {{ team.getInfo().getMainColor().getValue() }};
border-color: #666666;
{% endif %}
} }
#secondColor{ #second_color{
background-color: {{ team.secondColor.getValue() }}; background-color: {{ team.getInfo().getSecondColor().getValue() }};
{% if team.secondColor.getValue() == "#ffffff" %} border: solid;
border-color: #666666;
{% endif %}
} }
.container{ .container{
@ -54,6 +50,7 @@
height: 80px; height: 80px;
width: 80px; width: 80px;
} }
</style> </style>
</head> </head>
<body> <body>
@ -65,15 +62,21 @@
<div class="team container"> <div class="team container">
<div> <div>
<h1>{{ team.name }}</h1> <h1>{{ team.getInfo().getName() }}</h1>
<img src="{{ team.picture }}" alt="Logo d'équipe" class="logo"> <img src="{{ team.getInfo().getPicture() }}" alt="Logo d'équipe" class="logo">
</div> </div>
<div> <div>
<div class="color"><p>Couleur principale : </p><div class="square" id="mainColor"></div> </div> <div class="color"><p>Couleur principale : </p><div class="square" id="main_color"></div> </div>
<div class="color"><p>Couleur secondaire : </p><div class="square" id="secondColor"></div></div> <div class="color"><p>Couleur secondaire : </p><div class="square" id="second_color"></div></div>
</div> </div>
{% for m in team.members %}
<p> m.id </p> {% for m in team.listMembers() %}
<p> {{ m.getUserId() }} </p>
{% if m.getRole().isCoach() %}
<p> : Coach</p>
{% else %}
<p> : Joueur</p>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>

@ -10,7 +10,7 @@
<p>Aucune équipe n'a été trouvée</p> <p>Aucune équipe n'a été trouvée</p>
<div class="container"> <div class="container">
<h2>Chercher une équipe</h2> <h2>Chercher une équipe</h2>
<form action="/team/list" method="post"> <form action="/team/search" method="post">
<div class="form-group"> <div class="form-group">
<label for="name">Nom de l'équipe :</label> <label for="name">Nom de l'équipe :</label>
<input type="text" id="name" name="name" required> <input type="text" id="name" name="name" required>

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

@ -0,0 +1,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>

@ -70,10 +70,10 @@
<input type="text" id="name" name="name" required> <input type="text" id="name" name="name" required>
<label for= "picture">Logo:</label> <label for= "picture">Logo:</label>
<input type="text" id="picture" name="picture" required > <input type="text" id="picture" name="picture" required >
<label for="mainColor">Couleur principale</label> <label for="main_color">Couleur principale</label>
<input type="color" id="mainColor" name="mainColor" required> <input type="color" id="main_color" name="main_color" required>
<label for="secondColor">Couleur secondaire</label> <label for="second_color">Couleur secondaire</label>
<input type="color" id="secondColor" name="secondColor" required> <input type="color" id="second_color" name="second_color" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="submit" value="Confirmer"> <input type="submit" value="Confirmer">

@ -62,7 +62,7 @@
<div class="container"> <div class="container">
<h2>Chercher une équipe</h2> <h2>Chercher une équipe</h2>
<form action="/team/list" method="post"> <form action="/team/search" method="post">
<div class="form-group"> <div class="form-group">
<label for="name">Nom de l'équipe :</label> <label for="name">Nom de l'équipe :</label>
<input type="text" id="name" name="name" required> <input type="text" id="name" name="name" required>

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

needs some documentation here plz (little description)

needs some documentation here plz (little description)
*/
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

needs some documentation here plz (little description)

needs some documentation here plz (little description)
*/
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

needs some documentation here plz (little description)

needs some documentation here plz (little description)
*/
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;
}
}

@ -1,6 +1,6 @@
<?php <?php
namespace App\Data; namespace IQBall\Core\Data;
use InvalidArgumentException; use InvalidArgumentException;
@ -16,9 +16,6 @@ class Color {
*/ */
private function __construct(string $value) { private function __construct(string $value) {
if ($value < 0 || $value > 0xFFFFFF) {
throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF");
}
$this->hex = $value; $this->hex = $value;
} }

@ -1,16 +1,21 @@
<?php <?php
namespace App\Data; namespace IQBall\Core\Data;
/** /**
* information about a team member * information about a team member
*/ */
class Member { class Member {
/** /**
* @var int The member's user id * @var int The member's user account
*/ */
private int $userId; private int $userId;
/**
* @var int The member's team id
*/
private int $teamId;
/** /**
* @var MemberRole the member's role * @var MemberRole the member's role
*/ */
@ -20,8 +25,9 @@ class Member {
* @param int $userId * @param int $userId
* @param MemberRole $role * @param MemberRole $role
*/ */
public function __construct(int $userId, MemberRole $role) { public function __construct(int $userId, int $teamId, MemberRole $role) {
$this->userId = $userId; $this->userId = $userId;
$this->teamId = $teamId;
$this->role = $role; $this->role = $role;
} }
@ -39,4 +45,11 @@ class Member {
public function getRole(): MemberRole { public function getRole(): MemberRole {
return $this->role; return $this->role;
} }
/**
* @return int
*/
public function getTeamId(): int {
return $this->teamId;
}
} }

@ -1,13 +1,13 @@
<?php <?php
namespace App\Data; namespace IQBall\Core\Data;
use http\Exception\InvalidArgumentException; use InvalidArgumentException;
/** /**
* Enumeration class workaround * Enumeration class workaround
* As there is no enumerations in php 7.4, this class * As there is no enumerations in php 7.4, this class
* encapsulates an integer value and use it as an enumeration discriminant * encapsulates an integer value and use it as a variant discriminant
*/ */
final class MemberRole { final class MemberRole {
private const ROLE_PLAYER = 0; private const ROLE_PLAYER = 0;
@ -32,6 +32,27 @@ final class MemberRole {
return new MemberRole(MemberRole::ROLE_COACH); return new MemberRole(MemberRole::ROLE_COACH);
} }
public function name(): string {
switch ($this->value) {
case self::ROLE_COACH:
return "Coach";
case self::ROLE_PLAYER:
return "Player";
}
die("unreachable");
}
public static function fromName(string $name): ?MemberRole {
switch ($name) {
case "Coach":
return MemberRole::coach();
case "Player":
return MemberRole::player();
default:
return null;
}
}
private function isValid(int $val): bool { private function isValid(int $val): bool {
return ($val <= self::MAX and $val >= self::MIN); return ($val <= self::MAX and $val >= self::MIN);
} }

@ -1,21 +1,24 @@
<?php <?php
namespace App\Data; namespace IQBall\Core\Data;
class TacticInfo implements \JsonSerializable { class TacticInfo {
private int $id; private int $id;
private string $name; private string $name;
private int $creation_date; private int $creationDate;
private int $ownerId;
/** /**
* @param int $id * @param int $id
* @param string $name * @param string $name
* @param int $creation_date * @param int $creationDate
* @param int $ownerId
*/ */
public function __construct(int $id, string $name, int $creation_date) { public function __construct(int $id, string $name, int $creationDate, int $ownerId) {
$this->id = $id; $this->id = $id;
$this->name = $name; $this->name = $name;
$this->creation_date = $creation_date; $this->ownerId = $ownerId;
$this->creationDate = $creationDate;
} }
public function getId(): int { public function getId(): int {
@ -26,14 +29,15 @@ class TacticInfo implements \JsonSerializable {
return $this->name; return $this->name;
} }
public function getCreationTimestamp(): int {
return $this->creation_date;
}
/** /**
* @return array<string, mixed> * @return int
*/ */
public function jsonSerialize(): array { public function getOwnerId(): int {
return get_object_vars($this); return $this->ownerId;
} }
public function getCreationTimestamp(): int {
return $this->creationDate;
}
} }

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

@ -1,8 +1,8 @@
<?php <?php
namespace App\Data; namespace IQBall\Core\Data;
class Team { class TeamInfo {
private int $id; private int $id;
private string $name; private string $name;
private string $picture; private string $picture;
@ -10,66 +10,40 @@ class Team {
private Color $secondColor; private Color $secondColor;
/** /**
* @var Member[] maps users with their role * @param int $id
*/
private array $members;
/**
* @param string $name * @param string $name
* @param string $picture * @param string $picture
* @param Color $mainColor * @param Color $mainColor
* @param Color $secondColor * @param Color $secondColor
* @param Member[] $members
*/ */
public function __construct(int $id, string $name, string $picture, Color $mainColor, Color $secondColor, array $members = []) { public function __construct(int $id, string $name, string $picture, Color $mainColor, Color $secondColor) {
$this->id = $id; $this->id = $id;
$this->name = $name; $this->name = $name;
$this->picture = $picture; $this->picture = $picture;
$this->mainColor = $mainColor; $this->mainColor = $mainColor;
$this->secondColor = $secondColor; $this->secondColor = $secondColor;
$this->members = $members;
} }
/**
* @return int
*/
public function getId(): int { public function getId(): int {
return $this->id; return $this->id;
} }
/**
* @return string
*/
public function getName(): string { public function getName(): string {
return $this->name; return $this->name;
} }
/**
* @return string
*/
public function getPicture(): string { public function getPicture(): string {
return $this->picture; return $this->picture;
} }
/**
* @return Color
*/
public function getMainColor(): Color { public function getMainColor(): Color {
return $this->mainColor; return $this->mainColor;
} }
/**
* @return Color
*/
public function getSecondColor(): Color { public function getSecondColor(): Color {
return $this->secondColor; return $this->secondColor;
} }
/**
* @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

needs some documentation here plz

needs some documentation here plz
$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

needs some documentation here plz

needs some documentation here plz
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]);
}
}

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http; namespace IQBall\Core\Http;
class JsonHttpResponse extends HttpResponse { class JsonHttpResponse extends HttpResponse {
/** /**
@ -12,7 +12,7 @@ class JsonHttpResponse extends HttpResponse {
* @param mixed $payload * @param mixed $payload
*/ */
public function __construct($payload, int $code = HttpCodes::OK) { public function __construct($payload, int $code = HttpCodes::OK) {
parent::__construct($code); parent::__construct($code, []);
$this->payload = $payload; $this->payload = $payload;
} }

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

needs some documentation here plz (little description)

needs some documentation here plz (little description)
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;
use JsonSerializable; use JsonSerializable;
@ -33,8 +33,20 @@ class ValidationFail implements JsonSerializable {
return ["error" => $this->kind, "message" => $this->message]; return ["error" => $this->kind, "message" => $this->message];
} }
/**
* @param string $message
* @return ValidationFail validation fail for unknown resource access
*/
public static function notFound(string $message): ValidationFail { public static function notFound(string $message): ValidationFail {
return new ValidationFail("not found", $message); return new ValidationFail("Not found", $message);
}
/**
* @param string $message
* @return ValidationFail validation fail for unauthorized accesses
*/
public static function unauthorized(string $message = "Unauthorized"): ValidationFail {
return new ValidationFail("Unauthorized", $message);
} }
} }

@ -1,6 +1,6 @@
<?php <?php
namespace App\Validation; namespace IQBall\Core\Validation;
abstract class Validator { abstract class Validator {
/** /**

@ -1,6 +1,6 @@
<?php <?php
namespace App\Validation; namespace IQBall\Core\Validation;
/** /**
* A collection of standard validators * A collection of standard validators
@ -12,10 +12,18 @@ class Validators {
public static function regex(string $regex, ?string $msg = null): Validator { public static function regex(string $regex, ?string $msg = null): Validator {
return new SimpleFunctionValidator( return new SimpleFunctionValidator(
fn(string $str) => preg_match($regex, $str), fn(string $str) => preg_match($regex, $str),
fn(string $name) => [new FieldValidationFail($name, $msg == null ? "field does not validates pattern $regex" : $msg)] fn(string $name) => [new FieldValidationFail($name, $msg == null ? "le champ ne valide pas le pattern $regex" : $msg)]
); );
} }
public static function hex(?string $msg = null): Validator {
return self::regex('/#([0-9a-fA-F])/', $msg == null ? "le champ n'est pas un nombre hexadecimal valide" : $msg);
}
public static function hexColor(?string $msg = null): Validator {
return self::regex('/#([0-9a-fA-F]{6})/', $msg == null ? "le champ n'est pas une couleur valide" : $msg);
}
/** /**
* @return Validator a validator that validates strings that only contains numbers, letters, accents letters, `-` and `_`. * @return Validator a validator that validates strings that only contains numbers, letters, accents letters, `-` and `_`.
*/ */
@ -41,16 +49,23 @@ class Validators {
function (string $fieldName, string $str) use ($min, $max) { function (string $fieldName, string $str) use ($min, $max) {
$len = strlen($str); $len = strlen($str);
if ($len >= $max) { if ($len >= $max) {
return [new FieldValidationFail($fieldName, "field is longer than $max chars.")]; return [new FieldValidationFail($fieldName, "trop long, maximum $max caractères.")];
} }
if ($len < $min) { if ($len < $min) {
return [new FieldValidationFail($fieldName, "field is shorted than $min chars.")]; return [new FieldValidationFail($fieldName, "trop court, minimum $min caractères.")];
} }
return []; return [];
} }
); );
} }
public static function email(?string $msg = null): Validator {
return new SimpleFunctionValidator(
fn(string $str) => filter_var($str, FILTER_VALIDATE_EMAIL),
fn(string $name) => [new FieldValidationFail($name, $msg == null ? "addresse mail invalide" : $msg)]
);
}
public static function isInteger(): Validator { public static function isInteger(): Validator {
return self::regex("/^[0-9]+$/"); return self::regex("/^[0-9]+$/");

@ -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…
Cancel
Save