From a8b4b75e44583afe38238c82aff356e609a060f7 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Tue, 9 Jan 2024 16:18:16 +0100 Subject: [PATCH 01/22] remove global overflow hidden' --- src/App/react-display-file.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/App/react-display-file.php b/src/App/react-display-file.php index 7b09417..2dfcd11 100755 --- a/src/App/react-display-file.php +++ b/src/App/react-display-file.php @@ -31,7 +31,6 @@ height: 100%; width: 100%; margin: 0; - overflow: hidden; } From a123145acdf07a2aab499c1177f93ca0f23cf032 Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Tue, 9 Jan 2024 17:28:45 +0100 Subject: [PATCH 02/22] new branch where the documentation/conception will be updated --- Documentation/models.puml | 43 +++++++++++++++++++------------------- src/Core/Data/TeamInfo.php | 3 --- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/Documentation/models.puml b/Documentation/models.puml index 86ca699..727b2e3 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -29,18 +29,11 @@ class Account { class Member { - userId: int - teamId: int - - + __construct(role : MemberRole) + - role : string + + __construct(role : string) + getUserId(): int + getTeamId(): int - + getRole(): MemberRole -} - -Member --> "- role" MemberRole - -enum MemberRole { - PLAYER - COACH + + getRole(): string } @@ -48,28 +41,34 @@ class TeamInfo { - creationDate: int - name: string - picture: string - + - mainColor : string + - secondColor : string + getName(): string + getPicture(): string - + getMainColor(): Color - + getSecondColor(): Color + + getMainColor(): string + + getSecondColor(): string } -TeamInfo --> "- mainColor" Color -TeamInfo --> "- secondaryColor" Color - class Team { - getInfo(): TeamInfo - listMembers(): Member[] + + __construct() + + getInfo(): TeamInfo + + listMembers(): Member[] } Team --> "- info" TeamInfo Team --> "- members *" Member -class Color { - - value: int - - + getValue(): int +class User{ + - id : int + - name : string + - email : string + - profilePicture : string + + + __construct(id : int,name : string,email: string,profilePicture:string) + + getId() : id + + getName() : string + + getEmail() : string + + getProfilePicture() : string } @enduml \ No newline at end of file diff --git a/src/Core/Data/TeamInfo.php b/src/Core/Data/TeamInfo.php index 0f741fe..964990c 100644 --- a/src/Core/Data/TeamInfo.php +++ b/src/Core/Data/TeamInfo.php @@ -24,7 +24,6 @@ class TeamInfo implements \JsonSerializable { $this->secondColor = $secondColor; } - public function getId(): int { return $this->id; } @@ -48,6 +47,4 @@ class TeamInfo implements \JsonSerializable { public function jsonSerialize() { return get_object_vars($this); } - - } From 0640b9e46e91726abe78532f0dd9e35ac9bcef12 Mon Sep 17 00:00:00 2001 From: maxime Date: Thu, 11 Jan 2024 19:27:55 +0100 Subject: [PATCH 03/22] fix account.svg logo --- front/views/template/Header.tsx | 4 ++-- public/account.svg | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 public/account.svg diff --git a/front/views/template/Header.tsx b/front/views/template/Header.tsx index 2618d90..4bbd538 100644 --- a/front/views/template/Header.tsx +++ b/front/views/template/Header.tsx @@ -1,5 +1,5 @@ import { BASE } from "../../Constants" - +import accountSvg from "../../assets/account.svg" /** * * @param param0 username @@ -25,7 +25,7 @@ export function Header({ username }: { username: string }) { {/* */} { location.pathname = BASE + "/settings" }} diff --git a/public/account.svg b/public/account.svg deleted file mode 100644 index 70d7391..0000000 --- a/public/account.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From e1056f4ade452bda355072350c0b455cf583ddf9 Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Fri, 12 Jan 2024 11:34:29 +0100 Subject: [PATCH 04/22] updated old documentation --- Documentation/models.puml | 48 ++++++++++++----- Documentation/mvc/auth.puml | 32 ++++++------ Documentation/mvc/editor.puml | 38 ++++++++++++++ Documentation/mvc/team.puml | 96 +++++++++++++++++++++------------- Documentation/validation.puml | 30 ++++++++--- src/Core/Model/TacticModel.php | 8 --- 6 files changed, 172 insertions(+), 80 deletions(-) create mode 100644 Documentation/mvc/editor.puml diff --git a/Documentation/models.puml b/Documentation/models.puml index 727b2e3..b88b543 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -6,51 +6,75 @@ class TacticInfo { - creationDate: string - ownerId: string - content: string - + + + __construct(id:int,name:string,creationDate:int,ownerId:int,courtType:CourtType,content:string) + getId(): int + getOwnerId(): int + getCreationTimestamp(): int + getName(): string + getContent(): string + + getCourtType() : CourtType +} + +TacticInfo -->"- courtType" CourtType + +class CourtType{ + - value : int + - COURT_PLAIN : int {static} {frozen} + - COURT_HALF : int {static} {frozen} + + - __construct(val:int) + + plain() : CourtType {static} + + half() : CourtType {static} + + name() : string + + fromName(name:string) : CourtType + + isPlain() : bool + + isHalf() : bool } +note bottom: Basically an evoluated enum + class Account { - - email: string - token: string - - name: string - - id: int - + getMailAddress(): string - + getToken(): string - + getName(): string - + getId(): int + + __construct(token:string,user:User) + + getUser() : User + + getToken() : string } +Account -->"- user" User + class Member { - - userId: int - teamId: int - role : string + + __construct(role : string) - + getUserId(): int + + getUser(): User + getTeamId(): int + getRole(): string } +note bottom: Member's role is either "Coach" or "Player" + +Member -->"- user" User class TeamInfo { - - creationDate: int - name: string - picture: string - mainColor : string - secondColor : string + + + __construct(id:int,name:string,picture:string,mainColor:string,secondColor:string) + getName(): string + getPicture(): string + getMainColor(): string + getSecondColor(): string } +note left: Both team's color are the hex code of the color + class Team { - + __construct() + + __construct(info:TeamInfo,members: Member[]) + getInfo(): TeamInfo + listMembers(): Member[] } diff --git a/Documentation/mvc/auth.puml b/Documentation/mvc/auth.puml index 8a2bb1f..d6b56fa 100644 --- a/Documentation/mvc/auth.puml +++ b/Documentation/mvc/auth.puml @@ -7,26 +7,26 @@ class AuthController { + displayLogin() : HttpResponse + login(request : array , session : MutableSessionHandle) : HttpResponse } -AuthController --> "- model" AuthModel +AuthController *-- "- model" AuthModel class AuthModel { - +__construct(gateway : AccountGateway) - + register(username : string, password : string, confirmPassword : string, email : string, failures : array): Account - + generateToken() : string - + login(email : string, password : string) + + __construct(gateway : AccountGateway) + + register(username:string, password:string, confirmPassword:string, email:string, &failures:array): ?Account + generateToken() : string + + generateToken(): string + + login(email:string, password:string, &failures:array): ?Account } -AuthModel --> "- gateway" AccountGateway - -class AccountGateway { - -con : Connection - +__construct(con : Connection) - + insertAccount(name : string, email : string, hash : string, token : string) : int - + getRowsFromMail(email : string): array - + getHash(email : string) : array - + exists(email : string) : bool - + getAccountFromMail(email : string ): Account - + getAccountFromToken(email : string ): Account +AuthModel *-- "- gateway" AccountGateway +class AccountGateway{ + + __construct(con : Connexion) + + insertAccount(name:string, email:string, token:string, hash:string, profilePicture:string): int + + getRowsFromMail(email:string): ?array + + getHash(email:string): ?string + + exists(email:string): bool + + getAccountFromMail(email:string): ?Account + + getAccountFromToken(token:string): ?Account } +AccountGateway *--"- con" Connexion + @enduml \ No newline at end of file diff --git a/Documentation/mvc/editor.puml b/Documentation/mvc/editor.puml new file mode 100644 index 0000000..d684e5b --- /dev/null +++ b/Documentation/mvc/editor.puml @@ -0,0 +1,38 @@ +@startuml +class EditorController { + +__construct (model : TacticModel) + + openEditorFor(tactic:TacticInfo): ViewHttpResponse + + createNew(): ViewHttpResponse + + openTestEditor(courtType:CourtType): ViewHttpResponse + + createNewOfKind(type:CourtType, session:SessionHandle): ViewHttpResponse + + openEditor(id:int, session:SessionHandle): ViewHttpResponse +} +EditorController *-- "- model" TacticModel + +class TacticModel { + + TACTIC_DEFAULT_NAME:int {static}{frozen} + + __construct(tactics : TacticInfoGateway) + + makeNew(name:string, ownerId:int, type:CourtType): TacticInfo + + makeNewDefault(ownerId:int, type:CourtType): ?TacticInfo + + get(id:int): ?TacticInfo + + getLast(nb:int, ownerId:int): array + + getAll(ownerId:int): ?array + + updateName(id:int, name:string, authId:int): array + + updateContent(id:int, json:string): ?ValidationFail +} + +TacticModel *-- "- tactics" TacticInfoGateway + +class TacticInfoGateway{ + + __construct(con : Connexion) + + get(id:int): ?TacticInfo + + getLast(nb:int, ownerId:int): ?array + + getAll(ownerId:int): ?array + + insert(name:string, owner:int, type:CourtType): int + + updateName(id:int, name:string): bool + + updateContent(id:int, json:string): bool +} + +TacticInfoGateway *--"- con" Connexion + +@enduml \ No newline at end of file diff --git a/Documentation/mvc/team.puml b/Documentation/mvc/team.puml index ad5e201..8762ffe 100644 --- a/Documentation/mvc/team.puml +++ b/Documentation/mvc/team.puml @@ -1,63 +1,87 @@ @startuml -class Team { - - name: string - - picture: Url - - members: array - - + __construct(name : string, picture : string, mainColor : Colo, secondColor : Color) - + getName(): string - + getPicture(): Url - + getMainColor(): Color - + getSecondColor(): Color - + listMembers(): array -} -Team --> "- mainColor" Color -Team --> "- secondColor" Color -class Color { - - value: string - - __construct(value : string) - + getValue(): string - + from(value: string): Color - + tryFrom(value : string) : ?Color -} class TeamGateway{ - -- + + __construct(con : Connexion) + insert(name : string ,picture : string, mainColor : Color, secondColor : Color) + listByName(name : string): array + + getTeamById(id:int): ?TeamInfo + + getTeamIdByName(name:string): ?int + + deleteTeam(idTeam:int): void + + editTeam(idTeam:int, newName:string, newPicture:string, newMainColor:string, newSecondColor:string) + + getAll(user:int): array } TeamGateway *--"- con" Connexion -TeamGateway ..> Color + + +class MemberGateway{ + + + __construct(con : Connexion) + + insert(idTeam:int, userId:int, role:string): void + + getMembersOfTeam(teamId:int): array + + remove(idTeam:int, idMember:int): void + + isCoach(email:string, idTeam:int): bool + + isMemberOfTeam(idTeam:int, idCurrentUser:int): bool +} + +MemberGateway *--"- con" Connexion + +class AccountGateway{ + + __construct(con : Connexion) + + insertAccount(name:string, email:string, token:string, hash:string, profilePicture:string): int + + getRowsFromMail(email:string): ?array + + getHash(email:string): ?string + + exists(email:string): bool + + getAccountFromMail(email:string): ?Account + + getAccountFromToken(token:string): ?Account +} + +AccountGateway *--"- con" Connexion class TeamModel{ - --- + + __construct(gateway : TeamGateway) + createTeam(name : string,picture : string, mainColorValue : int, secondColorValue : int, errors : array) + + addMember(mail:string, teamId:int, role:string): int + listByName(name : string ,errors : array) : ?array - + displayTeam(id : int): Team + + getTeam(idTeam:int, idCurrentUser:int): ?Team + + deleteMember(idMember:int, teamId:int): int + + deleteTeam(email:string, idTeam:int): int + + isCoach(idTeam:int, email:string): bool + + editTeam(idTeam:int, newName:string, newPicture:string, newMainColor:string, newSecondColor:string) + + getAll(user:int): array } -TeamModel *--"- gateway" TeamGateway -TeamModel ..> Team -TeamModel ..> Color +TeamModel *--"- members" MemberGateway +TeamModel *--"- teams" TeamGateway +TeamModel *--"- teams" AccountGateway + class TeamController{ - - twig : Environement - -- - + __construct( model : TeamModel, twig : Environement) - + displaySubmitTeam() : HttpResponse - + submitTeam(request : array) : HttpResponse - + displayListTeamByName(): HttpResponse - + listTeamByName(request : array) : HttpResponse - + displayTeam(id : int): HttpResponse + + __construct( model : TeamModel) + + displayCreateTeam(session:SessionHandle): ViewHttpResponse + + displayDeleteMember(session:SessionHandle): ViewHttpResponse + + submitTeam(request:array, session:SessionHandle): HttpResponse + + displayListTeamByName(session:SessionHandle): ViewHttpResponse + + listTeamByName(request:array, session:SessionHandle): HttpResponse + + deleteTeamById(id:int, session:SessionHandle): HttpResponse + + displayTeam(id:int, session:SessionHandle): ViewHttpResponse + + displayAddMember(idTeam:int, session:SessionHandle): ViewHttpResponse + + addMember(idTeam:int, request:array, session:SessionHandle): HttpResponse + + deleteMember(idTeam:int, idMember:int, session:SessionHandle): HttpResponse + + displayEditTeam(idTeam:int, session:SessionHandle): ViewHttpResponse + + editTeam(idTeam:int, request:array, session:SessionHandle): HttpResponse } TeamController *--"- model" TeamModel + + + + class Connexion { } @enduml \ No newline at end of file diff --git a/Documentation/validation.puml b/Documentation/validation.puml index f509cf4..4595a5a 100644 --- a/Documentation/validation.puml +++ b/Documentation/validation.puml @@ -1,18 +1,20 @@ @startuml abstract class Validator { - + validate(name: string, val: mixed): array + + validate(name: string, val: mixed): array {abstract} + then(other: Validator): Validator } class ComposedValidator extends Validator { - - first: Validator - - then: Validator + __construct(first: Validator, then: Validator) - validate(name: string, val: mixed): array + + validate(name: string, val: mixed): array } +ComposedValidator -->"- first" Validator +ComposedValidator -->"- then" Validator + + class SimpleFunctionValidator extends Validator { - predicate: callable - error_factory: callable @@ -28,9 +30,9 @@ class ValidationFail implements JsonSerialize { + __construct(kind: string, message: string) + getMessage(): string + getKind(): string - + jsonSerialize() - + notFound(message: string): ValidationFail + + unauthorized(message:string): ValidationFail + + error(message:string): ValidationFail } class FieldValidationFail extends ValidationFail { @@ -49,13 +51,25 @@ class Validation { + validate(val: mixed, valName: string, failures: &array, validators: Validator...): bool } +Validation ..> Validator + class Validators { - --- + nonEmpty(): Validator + shorterThan(limit: int): Validator + userString(maxLen: int): Validator - ... + + regex(regex:string, msg:string): Validator + + hex(msg:string): Validator + + name(msg:string): Validator + + nameWithSpaces(): Validator + + lenBetween(min:int, max:int): Validator + + email(msg:string): Validator + + isInteger(): Validator + + isIntInRange(min:int, max:int): Validator + + isURL(): Validator } +Validators ..> Validator + + @enduml \ No newline at end of file diff --git a/src/Core/Model/TacticModel.php b/src/Core/Model/TacticModel.php index 7057e7f..4953fb2 100644 --- a/src/Core/Model/TacticModel.php +++ b/src/Core/Model/TacticModel.php @@ -11,7 +11,6 @@ use IQBall\Core\Validation\ValidationFail; class TacticModel { public const TACTIC_DEFAULT_NAME = "Nouvelle tactique"; - private TacticInfoGateway $tactics; /** @@ -52,13 +51,6 @@ class TacticModel { return $this->tactics->get($id); } - /** - * Return the nb last tactics created - * - * @param integer $nb - * @return array> - */ - /** * Return the nb last tactics * From f71835352d718daa12c2811cc23c3b1ce7150e5b Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Fri, 12 Jan 2024 13:45:54 +0100 Subject: [PATCH 05/22] updated editor's mvc conception --- Documentation/Conception.md | 2 +- Documentation/mvc/editor.puml | 6 +++++ Documentation/validation.puml | 4 ++-- src/Api/Controller/APIAuthController.php | 6 ++--- src/Api/Controller/APITacticController.php | 4 ++-- src/App/Control.php | 4 ++-- src/App/Controller/AuthController.php | 10 ++++----- src/App/Controller/TeamController.php | 22 +++++++++---------- .../{Validators.php => DefaultValidators.php} | 2 +- 9 files changed, 33 insertions(+), 27 deletions(-) rename src/Core/Validation/{Validators.php => DefaultValidators.php} (99%) diff --git a/Documentation/Conception.md b/Documentation/Conception.md index 68b4cd9..177be45 100644 --- a/Documentation/Conception.md +++ b/Documentation/Conception.md @@ -64,7 +64,7 @@ et de reporter le plus d'erreurs possibles lorsqu'une requête ne valide pas le public function doPostAction(array $form): HttpResponse { $failures = []; $req = HttpRequest::from($form, $failures, [ - 'email' => [Validators::email(), Validators::isLenBetween(6, 64)] + 'email' => [DefaultValidators::email(), DefaultValidators::isLenBetween(6, 64)] ]); if (!empty($failures)) { //ou $req == null diff --git a/Documentation/mvc/editor.puml b/Documentation/mvc/editor.puml index d684e5b..3e68d33 100644 --- a/Documentation/mvc/editor.puml +++ b/Documentation/mvc/editor.puml @@ -35,4 +35,10 @@ class TacticInfoGateway{ TacticInfoGateway *--"- con" Connexion +class TacticValidator{ + + validateAccess(tacticId:int, tactic:?TacticInfo, ownerId:int): ?ValidationFail {static} +} + +EditorController ..> TacticValidator + @enduml \ No newline at end of file diff --git a/Documentation/validation.puml b/Documentation/validation.puml index 4595a5a..64ac1a5 100644 --- a/Documentation/validation.puml +++ b/Documentation/validation.puml @@ -53,7 +53,7 @@ class Validation { Validation ..> Validator -class Validators { +class DefaultValidators { + nonEmpty(): Validator + shorterThan(limit: int): Validator + userString(maxLen: int): Validator @@ -68,7 +68,7 @@ class Validators { + isURL(): Validator } -Validators ..> Validator +DefaultValidators ..> Validator diff --git a/src/Api/Controller/APIAuthController.php b/src/Api/Controller/APIAuthController.php index fc0eef6..8e6291c 100644 --- a/src/Api/Controller/APIAuthController.php +++ b/src/Api/Controller/APIAuthController.php @@ -8,7 +8,7 @@ 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; +use IQBall\Core\Validation\DefaultValidators; class APIAuthController { private AuthModel $model; @@ -27,8 +27,8 @@ class APIAuthController { */ public function authorize(): HttpResponse { return Control::runChecked([ - "email" => [Validators::email(), Validators::lenBetween(5, 256)], - "password" => [Validators::lenBetween(6, 256)], + "email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)], + "password" => [DefaultValidators::lenBetween(6, 256)], ], function (HttpRequest $req) { $failures = []; $account = $this->model->login($req["email"], $req["password"], $failures); diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index a116add..2156538 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -10,7 +10,7 @@ use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Model\TacticModel; use IQBall\Core\Validation\FieldValidationFail; -use IQBall\Core\Validation\Validators; +use IQBall\Core\Validation\DefaultValidators; /** * API endpoint related to tactics @@ -33,7 +33,7 @@ class APITacticController { */ public function updateName(int $tactic_id, Account $account): HttpResponse { return Control::runChecked([ - "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], + "name" => [DefaultValidators::lenBetween(1, 50), DefaultValidators::nameWithSpaces()], ], function (HttpRequest $request) use ($tactic_id, $account) { $failures = $this->model->updateName($tactic_id, $request["name"], $account->getUser()->getId()); diff --git a/src/App/Control.php b/src/App/Control.php index 5c2fe0f..b8148bb 100644 --- a/src/App/Control.php +++ b/src/App/Control.php @@ -11,7 +11,7 @@ use IQBall\Core\Validation\Validator; class Control { /** * Runs given callback, if the request's json validates the given schema. - * @param array $schema an array of `fieldName => Validators` which represents the request object schema + * @param array $schema an array of `fieldName => DefaultValidators` which represents the request object schema * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. * THe callback must accept an HttpRequest, and return an HttpResponse object. * @return HttpResponse @@ -30,7 +30,7 @@ class Control { /** * Runs given callback, if the given request data array validates the given schema. * @param array $data the request's data array. - * @param array $schema an array of `fieldName => Validators` which represents the request object schema + * @param array $schema an array of `fieldName => DefaultValidators` which represents the request object schema * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. * THe callback must accept an HttpRequest, and return an HttpResponse object. * @return HttpResponse diff --git a/src/App/Controller/AuthController.php b/src/App/Controller/AuthController.php index cd89d11..4d9eebe 100644 --- a/src/App/Controller/AuthController.php +++ b/src/App/Controller/AuthController.php @@ -7,7 +7,7 @@ use IQBall\App\ViewHttpResponse; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Model\AuthModel; -use IQBall\Core\Validation\Validators; +use IQBall\Core\Validation\DefaultValidators; class AuthController { private AuthModel $model; @@ -32,10 +32,10 @@ class AuthController { 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)], + "username" => [DefaultValidators::name(), DefaultValidators::lenBetween(2, 32)], + "password" => [DefaultValidators::lenBetween(6, 256)], + "confirmpassword" => [DefaultValidators::lenBetween(6, 256)], + "email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)], ]); if (!empty($fails)) { return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]); diff --git a/src/App/Controller/TeamController.php b/src/App/Controller/TeamController.php index 4ab3fd7..048d182 100644 --- a/src/App/Controller/TeamController.php +++ b/src/App/Controller/TeamController.php @@ -11,7 +11,7 @@ use IQBall\Core\Http\HttpResponse; use IQBall\Core\Model\TeamModel; use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\ValidationFail; -use IQBall\Core\Validation\Validators; +use IQBall\Core\Validation\DefaultValidators; class TeamController { private TeamModel $model; @@ -48,10 +48,10 @@ class TeamController { 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()], + "name" => [DefaultValidators::lenBetween(1, 32), DefaultValidators::nameWithSpaces()], + "main_color" => [DefaultValidators::hexColor()], + "second_color" => [DefaultValidators::hexColor()], + "picture" => [DefaultValidators::isURL()], ]); if (!empty($failures)) { $badFields = []; @@ -84,7 +84,7 @@ class TeamController { public function listTeamByName(array $request, SessionHandle $session): HttpResponse { $errors = []; $request = HttpRequest::from($request, $errors, [ - "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], + "name" => [DefaultValidators::lenBetween(1, 32), DefaultValidators::nameWithSpaces()], ]); if (!empty($errors) && $errors[0] instanceof FieldValidationFail) { @@ -166,7 +166,7 @@ class TeamController { ], HttpCodes::FORBIDDEN); } $request = HttpRequest::from($request, $errors, [ - "email" => [Validators::email(), Validators::lenBetween(5, 256)], + "email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)], ]); if(!empty($errors)) { return ViewHttpResponse::twig('add_member.html.twig', ['badEmail' => true,'idTeam' => $idTeam]); @@ -226,10 +226,10 @@ class TeamController { } $failures = []; $request = HttpRequest::from($request, $failures, [ - "name" => [Validators::lenBetween(1, 32), Validators::nameWithSpaces()], - "main_color" => [Validators::hexColor()], - "second_color" => [Validators::hexColor()], - "picture" => [Validators::isURL()], + "name" => [DefaultValidators::lenBetween(1, 32), DefaultValidators::nameWithSpaces()], + "main_color" => [DefaultValidators::hexColor()], + "second_color" => [DefaultValidators::hexColor()], + "picture" => [DefaultValidators::isURL()], ]); if (!empty($failures)) { $badFields = []; diff --git a/src/Core/Validation/Validators.php b/src/Core/Validation/DefaultValidators.php similarity index 99% rename from src/Core/Validation/Validators.php rename to src/Core/Validation/DefaultValidators.php index 52bc08c..b6ffc38 100644 --- a/src/Core/Validation/Validators.php +++ b/src/Core/Validation/DefaultValidators.php @@ -5,7 +5,7 @@ namespace IQBall\Core\Validation; /** * A collection of standard validators */ -class Validators { +class DefaultValidators { /** * @return Validator a validator that validates a given regex */ From 9f8a5f5fc42d7df86267e3dfa8b783c9e76b60cd Mon Sep 17 00:00:00 2001 From: samuel Date: Tue, 9 Jan 2024 17:13:38 +0100 Subject: [PATCH 06/22] email and username persisted and RGPD checkbox --- src/App/Controller/AuthController.php | 10 +++++++--- src/App/Views/display_register.html.twig | 13 ++++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/App/Controller/AuthController.php b/src/App/Controller/AuthController.php index 7df241d..fced1d3 100644 --- a/src/App/Controller/AuthController.php +++ b/src/App/Controller/AuthController.php @@ -31,20 +31,24 @@ class AuthController { */ public function register(array $request, MutableSessionHandle $session): HttpResponse { $fails = []; - $request = HttpRequest::from($request, $fails, [ + $goodfield = []; + 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]); + if (!(in_array($request['username'], $fails)) or !(in_array($request['email'], $fails))) { + return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails,'username' => $request['username'],'email' => $request['email']]); + } } + $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(); diff --git a/src/App/Views/display_register.html.twig b/src/App/Views/display_register.html.twig index 38bdb43..7b883a5 100644 --- a/src/App/Views/display_register.html.twig +++ b/src/App/Views/display_register.html.twig @@ -62,6 +62,10 @@ text-align: right; } + .consentement{ + font-size: small; + } + #buttons{ display: flex; justify-content: center; @@ -95,15 +99,18 @@ {% endfor %} - + - +

+
Vous avez déjà un compte ? -
From 6cd52d19358efffca2e34f6e58233dc8bddafdbd Mon Sep 17 00:00:00 2001 From: samuel Date: Mon, 15 Jan 2024 10:47:45 +0100 Subject: [PATCH 07/22] PR OK --- src/App/Controller/AuthController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/App/Controller/AuthController.php b/src/App/Controller/AuthController.php index fced1d3..dbb72d6 100644 --- a/src/App/Controller/AuthController.php +++ b/src/App/Controller/AuthController.php @@ -31,7 +31,6 @@ class AuthController { */ public function register(array $request, MutableSessionHandle $session): HttpResponse { $fails = []; - $goodfield = []; HttpRequest::from($request, $fails, [ "username" => [Validators::name(), Validators::lenBetween(2, 32)], "password" => [Validators::lenBetween(6, 256)], From 88e0687cd267e4177a6b640acc4472169f82d350 Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Mon, 15 Jan 2024 16:20:29 +0100 Subject: [PATCH 08/22] updated the of the documentation + added a general markdown with a description for each diagram --- Documentation/Description.md | 82 +++++++++++++++++++++++++++ Documentation/architecture.puml | 60 ++++++++++++++++++++ Documentation/assets/architecture.svg | 1 + Documentation/assets/auth.svg | 1 + Documentation/assets/editor.svg | 1 + Documentation/assets/http.svg | 1 + Documentation/assets/models.svg | 1 + Documentation/assets/session.svg | 1 + Documentation/assets/team.svg | 1 + Documentation/assets/validation.svg | 1 + Documentation/http.puml | 32 ++++++++--- Documentation/session.puml | 27 +++++++++ Documentation/validation.puml | 6 ++ 13 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 Documentation/Description.md create mode 100644 Documentation/architecture.puml create mode 100644 Documentation/assets/architecture.svg create mode 100644 Documentation/assets/auth.svg create mode 100644 Documentation/assets/editor.svg create mode 100644 Documentation/assets/http.svg create mode 100644 Documentation/assets/models.svg create mode 100644 Documentation/assets/session.svg create mode 100644 Documentation/assets/team.svg create mode 100644 Documentation/assets/validation.svg create mode 100644 Documentation/session.puml diff --git a/Documentation/Description.md b/Documentation/Description.md new file mode 100644 index 0000000..ddb7dd0 --- /dev/null +++ b/Documentation/Description.md @@ -0,0 +1,82 @@ +# Welcome on the documentation's description + +## Let's get started with the architecture diagram. +![architecture diagram](./assets/architecture.svg) + +As you can see our entire application is build around three main package. +All of them contained in "src" package. +The core represent the main code of the web application. +It contains all the validation protocol, detailed below, the model of the imposed MVC architecture. +It also has a package named "data", it is a package of the structure of all the data we use in our application. +Of course there is package containing all the gateways as its name indicates. It is where we use the connection to our database. +Allowing to operate on it. + +The App now is more about the web application itself. +Having all the controllers of the MVC architecture the use the model, the validation system and the http system in the core. +It also calls the twig's views inside of App. Finally, it uses the package Session. This one replace the $_SESSION we all know in PHP. +Thanks to this we have a way cleaner use of all session's data. +Nevertheless, all the controllers call not only twig views but also react ones. +Those are present in the package "front", dispatched in several other packages. +Such as assets having all the image and stuff, model containing all the data's structure, style centralizing all css file and eventually components the last package used for the editor. + +Finally, we have the package "Api" that allows to share code and bind all the different third-hand application such as the web admin one. + +## Main class diagram. +![Class diagram](./assets/models.svg) + +You can see how our data is structured contained in the package "data" as explained right above. +There is two clear part. +First of all, the Tactic one. +We got a nice class named TacticInfo representing as it says the information about a tactic, nothing to discuss more about. +It associates an attribute of type "CourtType". This last is just an "evoluated" type of enum with some more features. +We had to do it this way because of the language PHP that doesn't implement such a thing as an enum. + +Now, let's discuss a much bigger part of the diagram. +In this part we find all the team logic. Actually, a team only have an array of members and a "TeamInfo". +The class "TeamInfo" exist to allows to split the data of the members. +The type Team does only link the information about a team and its members. +Talking about them, their class indicate what role they have (either Coach or Player) in the team. +Because a member is registered in the app, therefore he is a user of it. Represented by the type of the same name. +This class does only contain all the user's basic information. +The last class we have is the Account. It could directly be incorporated in User but we decided to split it the same way we did for the team. +Then, Account only has a user and a token which is an identifier. + +## Validation's class diagram +![validation's class diagram](./assets/Validation.svg) + +We implemented our own validation system, here it is! +For the validation methods (for instance those in DefaultValidators) we use lambda to instantiate a Validator. +In general, we use the implementation "SimpleFunctionValidator". +We reconize the strategy pattern. Indeed, we need a family of algorithms because we have many classes that only differ by the way they validate. +Futhermore, you may have notices the ComposedValidator that allows to chain several Validator. +We naturally used the composite pattern to solve this problem. +The other part of the diagram is about the failure a specific field's validation. +We have a concrete class to return a something more general. All the successors are just more precise about the failure. + +## Http's class diagram +![Http's class diagram](./assets/http.svg) +It were we centralize what the app can render, and what the api can receive. +Then, we got the "basic" response (HttpResponse) that just render a HttpCodes. +We have two successors for now. ViewHttpResponse render not only a code but also a view, either react or twig ones. +Finally, we have the JsonHttpResponse that renders, as it's name says, some Json. + +## Session's class diagram +![Session's class diagram](./assets/session.svg) + +It encapsulates the PHP's array "$_SESSION" and kind of replaces it. With two interfaces that dictate how a session should be handled, and same for a mutable one. + +## Model View Controller +All class diagram, separated by their range of action, of the imposed MVC architecture. +All of them have a controller that validates entries with the validation system and check the permission the user has,and whether or not actually do the action. +These controllers are composed by a Model that handle the pure data and is the point of contact between these and the gateways. +Speaking of which, Gateways are composing Models. They use the connection class to access the database and send their query. + +### Team +![team's mvc](./assets/team.svg) + +### Editor +![editor's mvc](./assets/editor.svg) + +### Authentification +![auth's mvc](./assets/auth.svg) + diff --git a/Documentation/architecture.puml b/Documentation/architecture.puml new file mode 100644 index 0000000..3e8f98f --- /dev/null +++ b/Documentation/architecture.puml @@ -0,0 +1,60 @@ +@startuml +'https://plantuml.com/component-diagram + +package front{ + package assets + package components + package model + package style + package views +} + +database sql{ + +} + +package src { + + package "Api"{ + + } + + package "App" { + package Controller + package Session + package Views + } + + package Core{ + package Data + package Gateway + package Http + package Model + package Validation + [Connection] + } + +} + +[sql] -- [Connection] + +[views] -- [style] +[views] -- [components] +[views] -- [assets] +[views] -- [model] + +[Gateway] -- [Connection] + +[Validation] -- [Controller] +[Controller] -- [Session] +[Controller] -- [Http] +[Controller] -- [Views] +[Controller] -- [views] +[Controller] -- [Model] +[Model] -- [Gateway] + +[Api] -- [Validation] +[Api] -- [Model] +[Api] -- [Http] + +@enduml \ No newline at end of file diff --git a/Documentation/assets/architecture.svg b/Documentation/assets/architecture.svg new file mode 100644 index 0000000..a93591b --- /dev/null +++ b/Documentation/assets/architecture.svg @@ -0,0 +1 @@ +frontsrcAppCoreassetscomponentsmodelstyleviewssqlApiControllerSessionViewsDataGatewayHttpModelValidationConnection \ No newline at end of file diff --git a/Documentation/assets/auth.svg b/Documentation/assets/auth.svg new file mode 100644 index 0000000..b25a316 --- /dev/null +++ b/Documentation/assets/auth.svg @@ -0,0 +1 @@ +AuthController__construct (model : AuthModel)displayRegister() : HttpResponseregister(request : array,session : MutableSessionHandle) : HttpResponsedisplayLogin() : HttpResponselogin(request : array , session : MutableSessionHandle) : HttpResponseAuthModel__construct(gateway : AccountGateway)register(username:string, password:string, confirmPassword:string, email:string, &failures:array): ?Account + generateToken() : stringgenerateToken(): stringlogin(email:string, password:string, &failures:array): ?AccountAccountGateway__construct(con : Connexion)insertAccount(name:string, email:string, token:string, hash:string, profilePicture:string): intgetRowsFromMail(email:string): ?arraygetHash(email:string): ?stringexists(email:string): boolgetAccountFromMail(email:string): ?AccountgetAccountFromToken(token:string): ?AccountConnexion- model- gateway- con \ No newline at end of file diff --git a/Documentation/assets/editor.svg b/Documentation/assets/editor.svg new file mode 100644 index 0000000..ddc8750 --- /dev/null +++ b/Documentation/assets/editor.svg @@ -0,0 +1 @@ +EditorController__construct (model : TacticModel)openEditorFor(tactic:TacticInfo): ViewHttpResponsecreateNew(): ViewHttpResponseopenTestEditor(courtType:CourtType): ViewHttpResponsecreateNewOfKind(type:CourtType, session:SessionHandle): ViewHttpResponseopenEditor(id:int, session:SessionHandle): ViewHttpResponseTacticModelTACTIC_DEFAULT_NAME:int {frozen}__construct(tactics : TacticInfoGateway)makeNew(name:string, ownerId:int, type:CourtType): TacticInfomakeNewDefault(ownerId:int, type:CourtType): ?TacticInfoget(id:int): ?TacticInfogetLast(nb:int, ownerId:int): arraygetAll(ownerId:int): ?arrayupdateName(id:int, name:string, authId:int): arrayupdateContent(id:int, json:string): ?ValidationFailTacticInfoGateway__construct(con : Connexion)get(id:int): ?TacticInfogetLast(nb:int, ownerId:int): ?arraygetAll(ownerId:int): ?arrayinsert(name:string, owner:int, type:CourtType): intupdateName(id:int, name:string): boolupdateContent(id:int, json:string): boolConnexionTacticValidatorvalidateAccess(tacticId:int, tactic:?TacticInfo, ownerId:int): ?ValidationFail- model- tactics- con \ No newline at end of file diff --git a/Documentation/assets/http.svg b/Documentation/assets/http.svg new file mode 100644 index 0000000..67ddc0a --- /dev/null +++ b/Documentation/assets/http.svg @@ -0,0 +1 @@ +HttpRequestdata: array__construct(data: array) offsetExists(offset: mixed): booloffsetGet(offset: mixed): mixedoffsetSet(offset: mixed, value: mixed)offsetUnset(offset: mixed) from(request: array, fails: &array, schema: array): HttpRequestfromPayload(fails: &array, schema: array): HttpRequestArrayAccessHttpResponsecode: intheaders : array__construct(code: int,headers:array)getCode(): intgetHeaders(): arrayredirect(url:string, code:int): HttpResponsefromCode(code: int): HttpResponseJsonHttpResponsepayload: mixed__construct(payload: mixed, code: int)getJson(): stringViewHttpResponseTWIG_VIEW: int {frozen}REACT_VIEW: int {frozen} file: stringarguments: arraykind: int__construct(kind: int, file: string, arguments: array, code: int)getViewKind(): intgetFile(): stringgetArguments(): array twig(file: string, arguments: array, code: int): ViewHttpResponsereact(file: string, arguments: array, code: int): ViewHttpResponseInto src/AppHttpCodesOK : int {frozen}FOUND : int {frozen}BAD_REQUEST : int {frozen}UNAUTHORIZED : int {frozen}FORBIDDEN : int {frozen}NOT_FOUND : int {frozen} \ No newline at end of file diff --git a/Documentation/assets/models.svg b/Documentation/assets/models.svg new file mode 100644 index 0000000..e691fa7 --- /dev/null +++ b/Documentation/assets/models.svg @@ -0,0 +1 @@ +TacticInfoid: intname: stringcreationDate: stringownerId: stringcontent: string__construct(id:int,name:string,creationDate:int,ownerId:int,courtType:CourtType,content:string)getId(): intgetOwnerId(): intgetCreationTimestamp(): intgetName(): stringgetContent(): stringgetCourtType() : CourtTypeCourtTypevalue : intCOURT_PLAIN : int {frozen}COURT_HALF : int {frozen}__construct(val:int)plain() : CourtTypehalf() : CourtTypename() : stringfromName(name:string) : CourtTypeisPlain() : boolisHalf() : boolBasically an evoluated enumAccounttoken: string__construct(token:string,user:User)getUser() : UsergetToken() : stringUserid : intname : stringemail : stringprofilePicture : string__construct(id : int,name : string,email: string,profilePicture:string)getId() : idgetName() : stringgetEmail() : stringgetProfilePicture() : stringMemberteamId: introle : string__construct(role : string)getUser(): UsergetTeamId(): intgetRole(): stringMember's role is either "Coach" or "Player"TeamInfoname: stringpicture: stringmainColor : stringsecondColor : string__construct(id:int,name:string,picture:string,mainColor:string,secondColor:string)getName(): stringgetPicture(): stringgetMainColor(): stringgetSecondColor(): stringBoth team's color are the hex code of the colorTeam__construct(info:TeamInfo,members: Member[])getInfo(): TeamInfolistMembers(): Member[]- courtType- user- user- info- members * \ No newline at end of file diff --git a/Documentation/assets/session.svg b/Documentation/assets/session.svg new file mode 100644 index 0000000..85ec404 --- /dev/null +++ b/Documentation/assets/session.svg @@ -0,0 +1 @@ +SessionHandlegetInitialTarget(): ?stringgetAccount(): ?AccountMutableSessionHandlesetInitialTarget(url:?string): voidsetAccount(account:Account): voiddestroy(): voidPhpSessionHandleinit(): selfgetAccount(): ?AccountgetInitialTarget(): ?stringsetAccount(account:Account): voidsetInitialTarget(url:?string): voiddestroy(): void \ No newline at end of file diff --git a/Documentation/assets/team.svg b/Documentation/assets/team.svg new file mode 100644 index 0000000..edf8a32 --- /dev/null +++ b/Documentation/assets/team.svg @@ -0,0 +1 @@ +TeamGateway__construct(con : Connexion)insert(name : string ,picture : string, mainColor : Color, secondColor : Color)listByName(name : string): arraygetTeamById(id:int): ?TeamInfogetTeamIdByName(name:string): ?intdeleteTeam(idTeam:int): voideditTeam(idTeam:int, newName:string, newPicture:string, newMainColor:string, newSecondColor:string)getAll(user:int): arrayConnexionMemberGateway__construct(con : Connexion)insert(idTeam:int, userId:int, role:string): voidgetMembersOfTeam(teamId:int): arrayremove(idTeam:int, idMember:int): voidisCoach(email:string, idTeam:int): boolisMemberOfTeam(idTeam:int, idCurrentUser:int): boolAccountGateway__construct(con : Connexion)insertAccount(name:string, email:string, token:string, hash:string, profilePicture:string): intgetRowsFromMail(email:string): ?arraygetHash(email:string): ?stringexists(email:string): boolgetAccountFromMail(email:string): ?AccountgetAccountFromToken(token:string): ?AccountTeamModel__construct(gateway : TeamGateway)createTeam(name : string,picture : string, mainColorValue : int, secondColorValue : int, errors : array)addMember(mail:string, teamId:int, role:string): intlistByName(name : string ,errors : array) : ?arraygetTeam(idTeam:int, idCurrentUser:int): ?TeamdeleteMember(idMember:int, teamId:int): intdeleteTeam(email:string, idTeam:int): intisCoach(idTeam:int, email:string): booleditTeam(idTeam:int, newName:string, newPicture:string, newMainColor:string, newSecondColor:string)getAll(user:int): arrayTeamController__construct( model : TeamModel)displayCreateTeam(session:SessionHandle): ViewHttpResponsedisplayDeleteMember(session:SessionHandle): ViewHttpResponsesubmitTeam(request:array, session:SessionHandle): HttpResponsedisplayListTeamByName(session:SessionHandle): ViewHttpResponselistTeamByName(request:array, session:SessionHandle): HttpResponsedeleteTeamById(id:int, session:SessionHandle): HttpResponsedisplayTeam(id:int, session:SessionHandle): ViewHttpResponsedisplayAddMember(idTeam:int, session:SessionHandle): ViewHttpResponseaddMember(idTeam:int, request:array, session:SessionHandle): HttpResponsedeleteMember(idTeam:int, idMember:int, session:SessionHandle): HttpResponsedisplayEditTeam(idTeam:int, session:SessionHandle): ViewHttpResponseeditTeam(idTeam:int, request:array, session:SessionHandle): HttpResponse- con- con- con- members- teams- teams- model \ No newline at end of file diff --git a/Documentation/assets/validation.svg b/Documentation/assets/validation.svg new file mode 100644 index 0000000..7f19c31 --- /dev/null +++ b/Documentation/assets/validation.svg @@ -0,0 +1 @@ +Validatorvalidate(name: string, val: mixed): arraythen(other: Validator): ValidatorComposedValidator__construct(first: Validator, then: Validator)validate(name: string, val: mixed): arraySimpleFunctionValidatorpredicate: callableerror_factory: callable__construct(predicate: callable, errorsFactory: callable)validate(name: string, val: mixed): arrayValidationFailkind: stringmessage: string__construct(kind: string, message: string)getMessage(): stringgetKind(): stringnotFound(message: string): ValidationFailunauthorized(message:string): ValidationFailerror(message:string): ValidationFailJsonSerializeFieldValidationFailfieldName: string__construct(fieldName: string, message: string)getFieldName(): stringjsonSerialize() invalidChars(fieldName: string): FieldValidationFailempty(fieldName: string): FieldValidationFailmissing(fieldName: string): FieldValidationFailValidation+ validate(val: mixed, valName: string, failures: &array, validators: Validator...): boolDefaultValidatorsnonEmpty(): ValidatorshorterThan(limit: int): ValidatoruserString(maxLen: int): Validatorregex(regex:string, msg:string): Validatorhex(msg:string): Validatorname(msg:string): ValidatornameWithSpaces(): ValidatorlenBetween(min:int, max:int): Validatoremail(msg:string): ValidatorisInteger(): ValidatorisIntInRange(min:int, max:int): ValidatorisURL(): ValidatorFunctionValidatorvalidate_fn: callable__construct(validate_fn:callable)validate(name:string, val:mixed): array- first- then \ No newline at end of file diff --git a/Documentation/http.puml b/Documentation/http.puml index 2f8e22b..e8fd478 100644 --- a/Documentation/http.puml +++ b/Documentation/http.puml @@ -15,37 +15,51 @@ class HttpRequest implements ArrayAccess { class HttpResponse { - code: int - + __construct(code: int) + - headers : array + + __construct(code: int,headers:array) + getCode(): int - - fromCode(code: int): HttpResponse + + getHeaders(): array + + redirect(url:string, code:int): HttpResponse {static} + + fromCode(code: int): HttpResponse {static} } class JsonHttpResponse extends HttpResponse { - payload: mixed - + __construct(payload: mixed, code: int = HttpCodes::OK) + + __construct(payload: mixed, code: int) + getJson(): string } class ViewHttpResponse extends HttpResponse { - + TWIG_VIEW: int {frozen} - + REACT_VIEW: int {frozen} + + TWIG_VIEW: int {frozen} {static} + + REACT_VIEW: int {frozen} {static} - file: string - arguments: array - kind: int - - __construct(kind: int, file: string, arguments: array, code: int = HttpCodes::OK) + - __construct(kind: int, file: string, arguments: array, code: int) + getViewKind(): int + getFile(): string + getArguments(): array - + twig(file: string, arguments: array, code: int = HttpCodes::OK): ViewHttpResponse - + react(file: string, arguments: array, code: int = HttpCodes::OK): ViewHttpResponse + + twig(file: string, arguments: array, code: int): ViewHttpResponse + + react(file: string, arguments: array, code: int): ViewHttpResponse } note right of ViewHttpResponse Into src/App end note +class HttpCodes{ + + OK : int {static} {frozen} + + FOUND : int {static} {frozen} + + BAD_REQUEST : int {static} {frozen} + + UNAUTHORIZED : int {static} {frozen} + + FORBIDDEN : int {static} {frozen} + + NOT_FOUND : int {static} {frozen} +} + +HttpCodes <.. ViewHttpResponse +HttpCodes <.. HttpResponse + @enduml \ No newline at end of file diff --git a/Documentation/session.puml b/Documentation/session.puml new file mode 100644 index 0000000..ccc8568 --- /dev/null +++ b/Documentation/session.puml @@ -0,0 +1,27 @@ +@startuml + +interface SessionHandle{ + + getInitialTarget(): ?string {abstract} + + getAccount(): ?Account {abstract} +} + +interface MutableSessionHandle{ + + setInitialTarget(url:?string): void + + setAccount(account:Account): void + + destroy(): void +} + +class PhpSessionHandle{ + + init(): self {static} + + getAccount(): ?Account + + getInitialTarget(): ?string + + setAccount(account:Account): void + + setInitialTarget(url:?string): void + + destroy(): void +} + + +PhpSessionHandle ..|> MutableSessionHandle +MutableSessionHandle ..|> SessionHandle + +@enduml \ No newline at end of file diff --git a/Documentation/validation.puml b/Documentation/validation.puml index 64ac1a5..77a8e60 100644 --- a/Documentation/validation.puml +++ b/Documentation/validation.puml @@ -70,6 +70,12 @@ class DefaultValidators { DefaultValidators ..> Validator +class FunctionValidator{ + - validate_fn: callable + + __construct(validate_fn:callable) + + validate(name:string, val:mixed): array +} +Validator <|-- FunctionValidator @enduml \ No newline at end of file From c596bfba719361b5f44c554286dcaf3f8c22b59f Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Mon, 15 Jan 2024 16:42:07 +0100 Subject: [PATCH 09/22] Fixed bugs --- src/App/Controller/AuthController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/App/Controller/AuthController.php b/src/App/Controller/AuthController.php index 386f896..3d16733 100644 --- a/src/App/Controller/AuthController.php +++ b/src/App/Controller/AuthController.php @@ -32,10 +32,10 @@ class AuthController { public function register(array $request, MutableSessionHandle $session): HttpResponse { $fails = []; 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)], + "username" => [DefaultValidators::name(), DefaultValidators::lenBetween(2, 32)], + "password" => [DefaultValidators::lenBetween(6, 256)], + "confirmpassword" => [DefaultValidators::lenBetween(6, 256)], + "email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)], ]); if (!empty($fails)) { From d9862b85c0240b520e5c473368173c021f96315f Mon Sep 17 00:00:00 2001 From: "mael.daim" Date: Tue, 16 Jan 2024 09:54:21 +0100 Subject: [PATCH 10/22] applied required review's changement --- Documentation/Description.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Documentation/Description.md b/Documentation/Description.md index ddb7dd0..fee90c5 100644 --- a/Documentation/Description.md +++ b/Documentation/Description.md @@ -21,7 +21,7 @@ Such as assets having all the image and stuff, model containing all the data's s Finally, we have the package "Api" that allows to share code and bind all the different third-hand application such as the web admin one. -## Main class diagram. +## Main data class diagram. ![Class diagram](./assets/models.svg) You can see how our data is structured contained in the package "data" as explained right above. @@ -33,7 +33,7 @@ We had to do it this way because of the language PHP that doesn't implement such Now, let's discuss a much bigger part of the diagram. In this part we find all the team logic. Actually, a team only have an array of members and a "TeamInfo". -The class "TeamInfo" exist to allows to split the data of the members. +The class "TeamInfo" only exists to split the team's information data (name, id etc) from the members. The type Team does only link the information about a team and its members. Talking about them, their class indicate what role they have (either Coach or Player) in the team. Because a member is registered in the app, therefore he is a user of it. Represented by the type of the same name. @@ -42,14 +42,14 @@ The last class we have is the Account. It could directly be incorporated in User Then, Account only has a user and a token which is an identifier. ## Validation's class diagram -![validation's class diagram](./assets/Validation.svg) +![validation's class diagram](./assets/validation.svg) We implemented our own validation system, here it is! For the validation methods (for instance those in DefaultValidators) we use lambda to instantiate a Validator. In general, we use the implementation "SimpleFunctionValidator". We reconize the strategy pattern. Indeed, we need a family of algorithms because we have many classes that only differ by the way they validate. -Futhermore, you may have notices the ComposedValidator that allows to chain several Validator. -We naturally used the composite pattern to solve this problem. +Futhermore, you may have notices the ComposedValidator that allows to chain several Validator. +We can see that this system uses the composite pattern The other part of the diagram is about the failure a specific field's validation. We have a concrete class to return a something more general. All the successors are just more precise about the failure. @@ -63,8 +63,7 @@ Finally, we have the JsonHttpResponse that renders, as it's name says, some Json ## Session's class diagram ![Session's class diagram](./assets/session.svg) -It encapsulates the PHP's array "$_SESSION" and kind of replaces it. With two interfaces that dictate how a session should be handled, and same for a mutable one. - +It encapsulates the PHP's array "$_SESSION". With two interfaces that dictate how a session should be handled, and same for a mutable one. ## Model View Controller All class diagram, separated by their range of action, of the imposed MVC architecture. All of them have a controller that validates entries with the validation system and check the permission the user has,and whether or not actually do the action. From ae3f14c663a0dcc8c185f3430ece2f36259bb9bf Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Tue, 16 Jan 2024 16:12:07 +0100 Subject: [PATCH 11/22] fix match type in hAPI::handleMatch --- src/Api/API.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Api/API.php b/src/Api/API.php index da00749..080db59 100644 --- a/src/Api/API.php +++ b/src/Api/API.php @@ -26,12 +26,12 @@ class API { /** - * @param array $match + * @param array|false $match * @param callable $tryGetAuthorization function to return an authorisation object for the given action (if required) * @return HttpResponse * @throws Exception */ - public static function handleMatch(array $match, callable $tryGetAuthorization): HttpResponse { + public static function handleMatch($match, callable $tryGetAuthorization): HttpResponse { if (!$match) { return new JsonHttpResponse([ValidationFail::notFound("not found")]); } From 740eb3d474bbf75eca4631d1d92ba8433b71cccf Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Tue, 16 Jan 2024 16:14:55 +0100 Subject: [PATCH 12/22] fix home button in home --- front/views/template/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/views/template/Header.tsx b/front/views/template/Header.tsx index 4bbd538..5c8cbcd 100644 --- a/front/views/template/Header.tsx +++ b/front/views/template/Header.tsx @@ -14,7 +14,7 @@ export function Header({ username }: { username: string }) { id="iqball" className="clickable" onClick={() => { - location.pathname = "/" + location.pathname = BASE + "/" }}> IQ Ball From 3ace79357866a6f5aa31c64da8ece20d17fd3d91 Mon Sep 17 00:00:00 2001 From: maxime Date: Tue, 16 Jan 2024 22:49:25 +0100 Subject: [PATCH 13/22] add eslint --- .eslintrc.js | 21 +++++++++++++++++++++ package.json | 13 ++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 .eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..aa4a8bc --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,21 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: [ + '@typescript-eslint', + 'react', + 'react-hooks' + ], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + 'plugin:react/jsx-runtime', + 'plugin:react-hooks/recommended' + ], + settings: { + react: { + version: 'detect' + } + } +}; \ No newline at end of file diff --git a/package.json b/package.json index 1380d59..966a0e9 100644 --- a/package.json +++ b/package.json @@ -24,16 +24,15 @@ "format": "prettier --config .prettierrc 'front' --write", "tsc": "tsc" }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, "devDependencies": { "@vitejs/plugin-react": "^4.1.0", "prettier": "^3.1.0", "typescript": "^5.2.2", - "vite-plugin-svgr": "^4.1.0" + "vite-plugin-svgr": "^4.1.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint": "^8.53.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0" } } From 9cde3af510d0f95819cf10373d72402502a6065d Mon Sep 17 00:00:00 2001 From: Override-6 Date: Fri, 8 Dec 2023 17:51:25 +0100 Subject: [PATCH 14/22] add basic api routes to get info on server, users and tactics --- Documentation/database_mld.puml | 2 +- public/api/index.php | 22 ++++++- sql/database.php | 18 ++++++ sql/setup-tables.sql | 15 ++--- src/Api/API.php | 20 ++++--- src/Api/Controller/APIAccountsController.php | 55 +++++++++++++++++ src/Api/Controller/APIAuthController.php | 3 +- src/Api/Controller/APIServerController.php | 45 ++++++++++++++ src/Api/Controller/APITacticController.php | 26 +++++++- src/App/App.php | 9 ++- src/App/Control.php | 21 +++++-- src/Core/Action.php | 26 +++++--- src/Core/Data/User.php | 18 +++++- src/Core/Gateway/AccountGateway.php | 62 ++++++++++++++++++-- src/Core/Gateway/MemberGateway.php | 4 +- src/Core/Gateway/TacticInfoGateway.php | 18 ++++++ src/Core/Model/AuthModel.php | 12 +++- src/Core/Model/TacticModel.php | 12 ++++ src/Core/Validation/DefaultValidators.php | 6 +- 19 files changed, 342 insertions(+), 52 deletions(-) create mode 100644 src/Api/Controller/APIAccountsController.php create mode 100644 src/Api/Controller/APIServerController.php diff --git a/Documentation/database_mld.puml b/Documentation/database_mld.puml index 2c33ce2..170fad7 100644 --- a/Documentation/database_mld.puml +++ b/Documentation/database_mld.puml @@ -7,7 +7,7 @@ object Account { email phoneNumber passwordHash - profilePicture + profile_picture } object TacticFolder { diff --git a/public/api/index.php b/public/api/index.php index da25013..91fa23d 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -6,7 +6,9 @@ require "../../sql/database.php"; require "../../src/index-utils.php"; use IQBall\Api\API; +use IQBall\Api\Controller\APIAccountsController; use IQBall\Api\Controller\APIAuthController; +use IQBall\Api\Controller\APIServerController; use IQBall\Api\Controller\APITacticController; use IQBall\App\Session\PhpSessionHandle; use IQBall\Core\Action; @@ -17,6 +19,8 @@ use IQBall\Core\Gateway\TacticInfoGateway; use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\TacticModel; +$basePath = get_public_path(__DIR__); + function getTacticController(): APITacticController { return new APITacticController(new TacticModel(new TacticInfoGateway(new Connection(get_database())))); } @@ -25,14 +29,30 @@ function getAuthController(): APIAuthController { return new APIAuthController(new AuthModel(new AccountGateway(new Connection(get_database())))); } +function getAccountController(): APIAccountsController { + $con = new Connection(get_database()); + return new APIAccountsController(new AccountGateway($con)); +} + +function getServerController(): APIServerController { + global $basePath; + return new APIServerController($basePath, get_database()); +} + function getRoutes(): AltoRouter { $router = new AltoRouter(); - $router->setBasePath(get_public_path(__DIR__)); + global $basePath; + $router->setBasePath($basePath); $router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize())); $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); $router->map("POST", "/tactic/[i:id]/save", Action::auth(fn(int $id, Account $acc) => getTacticController()->saveContent($id, $acc))); + $router->map("GET", "/admin/list-users", Action::admin(fn() => getAccountController()->listUsers($_GET))); + $router->map("GET", "/admin/user/[i:id]", Action::admin(fn(int $id) => getAccountController()->getUser($id))); + $router->map("GET", "/admin/user/[i:id]/space", Action::admin(fn(int $id) => getTacticController()->getUserTactics($id))); + $router->map("GET", "/admin/server-info", Action::admin(fn() => getServerController()->getServerInfo())); + return $router; } diff --git a/sql/database.php b/sql/database.php index 8f5aa9d..c301fda 100644 --- a/sql/database.php +++ b/sql/database.php @@ -1,5 +1,9 @@ insertAccount($name, $email, AuthModel::generateToken(), password_hash("123456", PASSWORD_DEFAULT)); + $accounts->setIsAdmin($id, true); + } +} diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 0d157d9..1a756f6 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -11,7 +11,8 @@ CREATE TABLE Account username varchar NOT NULL, token varchar UNIQUE NOT NULL, hash varchar NOT NULL, - profilePicture varchar NOT NULL + profile_picture varchar NOT NULL, + is_admin boolean DEFAULT false NOT NULL ); CREATE TABLE Tactic @@ -25,13 +26,6 @@ CREATE TABLE Tactic FOREIGN KEY (owner) REFERENCES Account ); -CREATE TABLE FormEntries -( - name varchar NOT NULL, - description varchar NOT NULL -); - - CREATE TABLE Team ( id integer PRIMARY KEY AUTOINCREMENT NOT NULL, @@ -41,11 +35,10 @@ CREATE TABLE Team second_color varchar NOT NULL ); - CREATE TABLE Member ( - id_team integer NOT NULL, - id_user integer NOT NULL, + id_team integer NOT NULL, + id_user integer NOT NULL, role text CHECK (role IN ('COACH', 'PLAYER')) NOT NULL, FOREIGN KEY (id_team) REFERENCES Team (id), FOREIGN KEY (id_user) REFERENCES Account (id) diff --git a/src/Api/API.php b/src/Api/API.php index 080db59..22b9566 100644 --- a/src/Api/API.php +++ b/src/Api/API.php @@ -4,6 +4,8 @@ namespace IQBall\Api; use Exception; use IQBall\Core\Action; +use IQBall\Core\Data\Account; +use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Validation\ValidationFail; @@ -27,7 +29,7 @@ class API { /** * @param array|false $match - * @param callable $tryGetAuthorization function to return an authorisation object for the given action (if required) + * @param callable(): Account $tryGetAuthorization function to return account authorisation for the given action (if required) * @return HttpResponse * @throws Exception */ @@ -41,15 +43,19 @@ class API { throw new Exception("routed action is not an AppAction object."); } - $auth = null; + $account = null; - if ($action->isAuthRequired()) { - $auth = call_user_func($tryGetAuthorization); - if ($auth == null) { - return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")]); + if ($action->getAuthType() != Action::NO_AUTH) { + $account = call_user_func($tryGetAuthorization); + if ($account == null) { + return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")], HttpCodes::UNAUTHORIZED); + } + + if ($action->getAuthType() == Action::AUTH_ADMIN && !$account->getUser()->isAdmin()) { + return new JsonHttpResponse([ValidationFail::unauthorized()], HttpCodes::UNAUTHORIZED); } } - return $action->run($match['params'], $auth); + return $action->run($match['params'], $account); } } diff --git a/src/Api/Controller/APIAccountsController.php b/src/Api/Controller/APIAccountsController.php new file mode 100644 index 0000000..3fe2de0 --- /dev/null +++ b/src/Api/Controller/APIAccountsController.php @@ -0,0 +1,55 @@ +accounts = $accounts; + } + + + /** + * @param array $request + * @return HttpResponse + */ + public function listUsers(array $request): HttpResponse { + return Control::runCheckedFrom($request, [ + 'start' => [DefaultValidators::isUnsignedInteger()], + 'n' => [DefaultValidators::isUnsignedInteger()], + ], function (HttpRequest $req) { + $accounts = $this->accounts->listAccounts(intval($req['start']), intval($req['n'])); + $response = array_map(fn(Account $acc) => $acc->getUser(), $accounts); + return new JsonHttpResponse($response); + }, true); + } + + + /** + * @param int $userId + * @return HttpResponse given user information. + */ + public function getUser(int $userId): HttpResponse { + $acc = $this->accounts->getAccount($userId); + + if ($acc == null) { + return new JsonHttpResponse([ValidationFail::notFound("User not found")], HttpCodes::NOT_FOUND); + } + + return new JsonHttpResponse($acc->getUser()); + } +} diff --git a/src/Api/Controller/APIAuthController.php b/src/Api/Controller/APIAuthController.php index 8e6291c..e257844 100644 --- a/src/Api/Controller/APIAuthController.php +++ b/src/Api/Controller/APIAuthController.php @@ -38,7 +38,6 @@ class APIAuthController { } return new JsonHttpResponse(["authorization" => $account->getToken()]); - }); + }, true); } - } diff --git a/src/Api/Controller/APIServerController.php b/src/Api/Controller/APIServerController.php new file mode 100644 index 0000000..1c82d3e --- /dev/null +++ b/src/Api/Controller/APIServerController.php @@ -0,0 +1,45 @@ +basePath = $basePath; + $this->pdo = $pdo; + } + + private function countLines(string $table): int { + $stmnt = $this->pdo->prepare("SELECT count(*) FROM $table"); + $stmnt->execute(); + $res = $stmnt->fetch(\PDO::FETCH_BOTH); + return $res[0]; + } + + + /** + * @return HttpResponse some (useless) information about the server + */ + public function getServerInfo(): HttpResponse { + + return new JsonHttpResponse([ + 'base_path' => $this->basePath, + 'date' => (int) gettimeofday(true) * 1000, + 'database' => [ + 'accounts' => $this->countLines("Account") . " line(s)", + 'tactics' => $this->countLines("Tactic") . " line(s)", + 'teams' => $this->countLines("Team") . " line(s)", + ], + ]); + } + +} diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 2156538..0d99f41 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -4,12 +4,12 @@ namespace IQBall\Api\Controller; use IQBall\App\Control; use IQBall\Core\Data\Account; +use IQBall\Core\Data\TacticInfo; 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\FieldValidationFail; use IQBall\Core\Validation\DefaultValidators; /** @@ -44,7 +44,7 @@ class APITacticController { } return HttpResponse::fromCode(HttpCodes::OK); - }); + }, true); } /** @@ -60,6 +60,26 @@ class APITacticController { return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST); } return HttpResponse::fromCode(HttpCodes::OK); - }); + }, true); } + + + /** + * @param int $userId + * @return HttpResponse given user information. + */ + public function getUserTactics(int $userId): HttpResponse { + $tactics = $this->model->listAllOf($userId); + + $response = array_map(fn(TacticInfo $t) => [ + 'id' => $t->getId(), + 'name' => $t->getName(), + 'court' => $t->getCourtType(), + 'creation_date' => $t->getCreationDate(), + ], $tactics); + + return new JsonHttpResponse($response); + } + + } diff --git a/src/App/App.php b/src/App/App.php index cd3c293..cba59de 100644 --- a/src/App/App.php +++ b/src/App/App.php @@ -4,8 +4,10 @@ namespace IQBall\App; use IQBall\App\Session\MutableSessionHandle; use IQBall\Core\Action; +use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; +use IQBall\Core\Validation\ValidationFail; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; @@ -77,13 +79,18 @@ class App { * @return HttpResponse */ public static function runAction(string $authRoute, Action $action, array $params, MutableSessionHandle $session): HttpResponse { - if ($action->isAuthRequired()) { + if ($action->getAuthType() != Action::NO_AUTH) { $account = $session->getAccount(); if ($account == null) { // put in the session the initial url the user wanted to get $session->setInitialTarget($_SERVER['REQUEST_URI']); return HttpResponse::redirect($authRoute); } + + if ($action->getAuthType() == Action::AUTH_ADMIN && !$account->getUser()->isAdmin()) { + return new JsonHttpResponse([ValidationFail::unauthorized()], HttpCodes::UNAUTHORIZED); + } + } return $action->run($params, $session); diff --git a/src/App/Control.php b/src/App/Control.php index b8148bb..0e517e4 100644 --- a/src/App/Control.php +++ b/src/App/Control.php @@ -5,6 +5,7 @@ namespace IQBall\App; use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; +use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\Validator; @@ -13,18 +14,24 @@ class Control { * Runs given callback, if the request's json validates the given schema. * @param array $schema an array of `fieldName => DefaultValidators` which represents the request object schema * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. - * THe callback must accept an HttpRequest, and return an HttpResponse object. + * The callback must accept an HttpRequest, and return an HttpResponse object. + * @param bool $errorInJson if set to true, the returned response will be a JsonHttpResponse if the schema isn't validated * @return HttpResponse */ - public static function runChecked(array $schema, callable $run): HttpResponse { + public static function runChecked(array $schema, callable $run, bool $errorInJson = false): HttpResponse { $request_body = file_get_contents('php://input'); $payload_obj = json_decode($request_body); if (!$payload_obj instanceof \stdClass) { $fail = new ValidationFail("bad-payload", "request body is not a valid json object"); + + if ($errorInJson) { + return new JsonHttpResponse([$fail]); + } + return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST); } $payload = get_object_vars($payload_obj); - return self::runCheckedFrom($payload, $schema, $run); + return self::runCheckedFrom($payload, $schema, $run, $errorInJson); } /** @@ -32,14 +39,18 @@ class Control { * @param array $data the request's data array. * @param array $schema an array of `fieldName => DefaultValidators` which represents the request object schema * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. - * THe callback must accept an HttpRequest, and return an HttpResponse object. + * The callback must accept an HttpRequest, and return an HttpResponse object. + * @param bool $errorInJson if set to true, the returned response will be a JsonHttpResponse if the schema isn't validated * @return HttpResponse */ - public static function runCheckedFrom(array $data, array $schema, callable $run): HttpResponse { + public static function runCheckedFrom(array $data, array $schema, callable $run, bool $errorInJson = false): HttpResponse { $fails = []; $request = HttpRequest::from($data, $fails, $schema); if (!empty($fails)) { + if ($errorInJson) { + return new JsonHttpResponse($fails); + } return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST); } diff --git a/src/Core/Action.php b/src/Core/Action.php index 35721c1..df40ea9 100644 --- a/src/Core/Action.php +++ b/src/Core/Action.php @@ -9,23 +9,27 @@ use IQBall\Core\Http\HttpResponse; * @template S session */ class Action { + public const NO_AUTH = 1; + public const AUTH_USER = 2; + public const AUTH_ADMIN = 3; + /** * @var callable(mixed[], S): HttpResponse $action action to call */ protected $action; - private bool $isAuthRequired; + private int $authType; /** * @param callable(mixed[], S): HttpResponse $action */ - protected function __construct(callable $action, bool $isAuthRequired) { + protected function __construct(callable $action, int $authType) { $this->action = $action; - $this->isAuthRequired = $isAuthRequired; + $this->authType = $authType; } - public function isAuthRequired(): bool { - return $this->isAuthRequired; + public function getAuthType(): int { + return $this->authType; } /** @@ -45,7 +49,7 @@ class Action { * @return Action an action that does not require to have an authorization. */ public static function noAuth(callable $action): Action { - return new Action($action, false); + return new Action($action, self::NO_AUTH); } /** @@ -53,6 +57,14 @@ class Action { * @return Action an action that does require to have an authorization. */ public static function auth(callable $action): Action { - return new Action($action, true); + return new Action($action, self::AUTH_USER); + } + + /** + * @param callable(mixed[], S): HttpResponse $action + * @return Action an action that does require to have an authorization, and to be an administrator. + */ + public static function admin(callable $action): Action { + return new Action($action, self::AUTH_ADMIN); } } diff --git a/src/Core/Data/User.php b/src/Core/Data/User.php index 71e0dd1..02a44c0 100644 --- a/src/Core/Data/User.php +++ b/src/Core/Data/User.php @@ -2,8 +2,6 @@ namespace IQBall\Core\Data; -use _PHPStan_4c4f22f13\Nette\Utils\Json; - class User implements \JsonSerializable { /** * @var string $email user's mail address @@ -25,17 +23,31 @@ class User implements \JsonSerializable { */ private string $profilePicture; + /** + * @var bool isAdmin + */ + private bool $isAdmin; + /** * @param string $email * @param string $name * @param int $id * @param string $profilePicture + * @param bool $isAdmin */ - public function __construct(string $email, string $name, int $id, string $profilePicture) { + public function __construct(string $email, string $name, int $id, string $profilePicture, bool $isAdmin) { $this->email = $email; $this->name = $name; $this->id = $id; $this->profilePicture = $profilePicture; + $this->isAdmin = $isAdmin; + } + + /** + * @return bool + */ + public function isAdmin(): bool { + return $this->isAdmin; } /** diff --git a/src/Core/Gateway/AccountGateway.php b/src/Core/Gateway/AccountGateway.php index a9c3e18..6251b85 100644 --- a/src/Core/Gateway/AccountGateway.php +++ b/src/Core/Gateway/AccountGateway.php @@ -2,6 +2,7 @@ namespace IQBall\Core\Gateway; +use Cassandra\PreparedStatement; use IQBall\Core\Connection; use IQBall\Core\Data\Account; use IQBall\Core\Data\User; @@ -18,7 +19,7 @@ class AccountGateway { } public function insertAccount(string $name, string $email, string $token, string $hash, string $profilePicture): int { - $this->con->exec("INSERT INTO Account(username, hash, email, token,profilePicture) VALUES (:username,:hash,:email,:token,:profilePic)", [ + $this->con->exec("INSERT INTO Account(username, hash, email, token,profile_picture) VALUES (:username,:hash,:email,:token,:profilePic)", [ ':username' => [$name, PDO::PARAM_STR], ':hash' => [$hash, PDO::PARAM_STR], ':email' => [$email, PDO::PARAM_STR], @@ -28,6 +29,22 @@ class AccountGateway { return intval($this->con->lastInsertId()); } + + /** + * promote or demote a user to server administrator + * @param int $id + * @param bool $isAdmin true to promote, false to demote + * @return bool true if the given user exists + */ + public function setIsAdmin(int $id, bool $isAdmin): bool { + $stmnt = $this->con->prepare("UPDATE Account SET is_admin = :is_admin WHERE id = :id"); + $stmnt->bindValue(':is_admin', $isAdmin); + $stmnt->bindValue(':id', $id); + $stmnt->execute(); + + return $stmnt->rowCount() > 0; + } + /** * @param string $email * @return array|null @@ -66,7 +83,7 @@ class AccountGateway { return null; } - return new Account($acc["token"], new User($email, $acc["username"], $acc["id"], $acc["profilePicture"])); + return new Account($acc["token"], new User($email, $acc["username"], $acc["id"], $acc["profile_picture"], $acc['is_admin'])); } /** @@ -74,13 +91,48 @@ class AccountGateway { * @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)) { + $stmnt = $this->con->prepare("SELECT * FROM Account WHERE token = :token"); + $stmnt->bindValue(':token', $token); + return $this->getAccountFrom($stmnt); + } + + /** + * @param int $id get an account from given identifier + * @return Account|null + */ + public function getAccount(int $id): ?Account { + $stmnt = $this->con->prepare("SELECT * FROM Account WHERE id = :id"); + $stmnt->bindValue(':id', $id); + return $this->getAccountFrom($stmnt); + } + + private function getAccountFrom(\PDOStatement $stmnt): ?Account { + $stmnt->execute(); + $acc = $stmnt->fetch(PDO::FETCH_ASSOC); + + if ($acc == null) { return null; } - return new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profilePicture"])); + return new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $acc["is_admin"])); } + /** + * Return a list containing n accounts from a given starting index + * + * @param integer $n the number of accounts to retrieve + * @param int $start starting index of the list content + * @return Account[] + */ + public function listAccounts(int $start, int $n): ?array { + $res = $this->con->fetch( + "SELECT * FROM Account ORDER BY email LIMIT :offset, :n", + [ + ":offset" => [$start, PDO::PARAM_INT], + ":n" => [$n, PDO::PARAM_INT], + ] + ); + return array_map(fn(array $acc) => new Account($acc["email"], new User($acc["username"], $acc["token"], $acc["id"], $acc["profile_picture"], $acc["is_admin"])), $res); + } } diff --git a/src/Core/Gateway/MemberGateway.php b/src/Core/Gateway/MemberGateway.php index a5116e8..98d2d41 100644 --- a/src/Core/Gateway/MemberGateway.php +++ b/src/Core/Gateway/MemberGateway.php @@ -41,12 +41,12 @@ class MemberGateway { */ public function getMembersOfTeam(int $teamId): array { $rows = $this->con->fetch( - "SELECT a.id,a.email,a.username,a.profilePicture,m.role FROM Account a,team t,Member m WHERE t.id = :id AND m.id_team = t.id AND m.id_user = a.id", + "SELECT a.id,a.email,a.username,a.profile_picture,m.role 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(new User($row['email'], $row['username'], $row['id'], $row['profilePicture']), $teamId, $row['role']), $rows); + return array_map(fn($row) => new Member(new User($row['email'], $row['username'], $row['id'], $row['profile_picture'], $row['is_admin']), $teamId, $row['role']), $rows); } /** diff --git a/src/Core/Gateway/TacticInfoGateway.php b/src/Core/Gateway/TacticInfoGateway.php index 08302c9..d4b81e0 100644 --- a/src/Core/Gateway/TacticInfoGateway.php +++ b/src/Core/Gateway/TacticInfoGateway.php @@ -83,6 +83,24 @@ class TacticInfoGateway { return $res; } + + /** + * Return a list containing the nth last tactics of a given user id + * + * @param integer $user_id + * @return TacticInfo[] + */ + public function listAllOf(int $user_id): array { + $res = $this->con->fetch( + "SELECT * FROM Tactic WHERE owner = :owner_id ORDER BY creation_date DESC", + [ + ":owner_id" => [$user_id, PDO::PARAM_STR], + ] + ); + return array_map(fn(array $t) => new TacticInfo($t['id'], $t["name"], strtotime($t["creation_date"]), $t["owner"], CourtType::fromName($t['court_type']), $t['content']), $res); + } + + /** * @param string $name * @param int $owner diff --git a/src/Core/Model/AuthModel.php b/src/Core/Model/AuthModel.php index bc29248..b937924 100644 --- a/src/Core/Model/AuthModel.php +++ b/src/Core/Model/AuthModel.php @@ -29,7 +29,13 @@ class AuthModel { * @return Account|null the registered account or null if failures occurred * @throws Exception */ - public function register(string $username, string $password, string $confirmPassword, string $email, array &$failures): ?Account { + public function register( + string $username, + string $password, + string $confirmPassword, + string $email, + array &$failures + ): ?Account { if ($password != $confirmPassword) { $failures[] = new FieldValidationFail("confirmpassword", "Le mot de passe et la confirmation ne sont pas les mêmes."); @@ -47,7 +53,7 @@ class AuthModel { $token = $this->generateToken(); $accountId = $this->gateway->insertAccount($username, $email, $token, $hash, self::DEFAULT_PROFILE_PICTURE); - return new Account($token, new User($email, $username, $accountId, self::DEFAULT_PROFILE_PICTURE)); + return new Account($token, new User($email, $username, $accountId, self::DEFAULT_PROFILE_PICTURE, false)); } /** @@ -55,7 +61,7 @@ class AuthModel { * @return string * @throws Exception */ - private function generateToken(): string { + public static function generateToken(): string { return base64_encode(random_bytes(64)); } diff --git a/src/Core/Model/TacticModel.php b/src/Core/Model/TacticModel.php index 4953fb2..590a106 100644 --- a/src/Core/Model/TacticModel.php +++ b/src/Core/Model/TacticModel.php @@ -62,6 +62,18 @@ class TacticModel { return $this->tactics->getLast($nb, $ownerId); } + + /** + * Return a list containing all the tactics of a given user + * NOTE: if given user id does not match any user, this function returns an empty array + * + * @param integer $user_id + * @return TacticInfo[] | null + */ + public function listAllOf(int $user_id): ?array { + return$this->tactics->listAllOf($user_id); + } + /** * Get all the tactics of the owner * diff --git a/src/Core/Validation/DefaultValidators.php b/src/Core/Validation/DefaultValidators.php index b6ffc38..67d5da2 100644 --- a/src/Core/Validation/DefaultValidators.php +++ b/src/Core/Validation/DefaultValidators.php @@ -68,7 +68,11 @@ class DefaultValidators { public static function isInteger(): Validator { - return self::regex("/^[0-9]+$/"); + return self::regex("/^-[0-9]+$/", "field is not an integer"); + } + + public static function isUnsignedInteger(): Validator { + return self::regex("/^[0-9]+$/", "field is not an unsigned integer"); } public static function isIntInRange(int $min, int $max): Validator { From 5df30ee415146894990fcc0444a3335503f2b0dc Mon Sep 17 00:00:00 2001 From: Override-6 Date: Fri, 8 Dec 2023 22:54:34 +0100 Subject: [PATCH 15/22] add remove, update and add new accounts --- public/api/index.php | 16 ++++-- sql/database.php | 2 +- src/Api/API.php | 6 +- src/Api/Controller/APIAccountsController.php | 59 ++++++++++++++++++-- src/Api/Controller/APIAuthController.php | 2 +- src/App/Control.php | 4 +- src/App/Controller/AuthController.php | 23 ++++++-- src/Core/Gateway/AccountGateway.php | 39 +++++++++++-- src/Core/Http/HttpResponse.php | 4 +- src/Core/Model/AuthModel.php | 33 +++++------ src/Core/Validation/DefaultValidators.php | 57 +++++++++++++++++++ 11 files changed, 198 insertions(+), 47 deletions(-) diff --git a/public/api/index.php b/public/api/index.php index 91fa23d..226e8f1 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -31,7 +31,8 @@ function getAuthController(): APIAuthController { function getAccountController(): APIAccountsController { $con = new Connection(get_database()); - return new APIAccountsController(new AccountGateway($con)); + $gw = new AccountGateway($con); + return new APIAccountsController(new AuthModel($gw), $gw); } function getServerController(): APIServerController { @@ -48,10 +49,13 @@ function getRoutes(): AltoRouter { $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); $router->map("POST", "/tactic/[i:id]/save", Action::auth(fn(int $id, Account $acc) => getTacticController()->saveContent($id, $acc))); - $router->map("GET", "/admin/list-users", Action::admin(fn() => getAccountController()->listUsers($_GET))); - $router->map("GET", "/admin/user/[i:id]", Action::admin(fn(int $id) => getAccountController()->getUser($id))); - $router->map("GET", "/admin/user/[i:id]/space", Action::admin(fn(int $id) => getTacticController()->getUserTactics($id))); - $router->map("GET", "/admin/server-info", Action::admin(fn() => getServerController()->getServerInfo())); + $router->map("GET", "/admin/list-users", Action::noAuth(fn() => getAccountController()->listUsers($_GET))); + $router->map("GET", "/admin/user/[i:id]", Action::noAuth(fn(int $id) => getAccountController()->getUser($id))); + $router->map("GET", "/admin/user/[i:id]/space", Action::noAuth(fn(int $id) => getTacticController()->getUserTactics($id))); + $router->map("POST", "/admin/user/add", Action::noAuth(fn() => getAccountController()->addUser())); + $router->map("POST", "/admin/user/remove-all", Action::noAuth(fn() => getAccountController()->removeUsers())); + $router->map("POST", "/admin/user/[i:id]/update", Action::noAuth(fn(int $id) => getAccountController()->updateUser($id))); + $router->map("GET", "/admin/server-info", Action::noAuth(fn() => getServerController()->getServerInfo())); return $router; } @@ -76,4 +80,4 @@ function tryGetAuthorization(): ?Account { return $gateway->getAccountFromToken($token); } -Api::render(API::handleMatch(getRoutes()->match(), fn() => tryGetAuthorization())); +Api::consume(API::handleMatch(getRoutes()->match(), fn() => tryGetAuthorization())); diff --git a/sql/database.php b/sql/database.php index c301fda..af94ef2 100644 --- a/sql/database.php +++ b/sql/database.php @@ -39,7 +39,7 @@ function init_database(PDO $pdo): void { foreach ($defaultAccounts as $name) { $email = "$name@mail.com"; - $id = $accounts->insertAccount($name, $email, AuthModel::generateToken(), password_hash("123456", PASSWORD_DEFAULT)); + $id = $accounts->insertAccount($name, $email, AuthModel::generateToken(), password_hash("123456", PASSWORD_DEFAULT), "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png"); $accounts->setIsAdmin($id, true); } } diff --git a/src/Api/API.php b/src/Api/API.php index 22b9566..b955963 100644 --- a/src/Api/API.php +++ b/src/Api/API.php @@ -11,9 +11,13 @@ use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Validation\ValidationFail; class API { - public static function render(HttpResponse $response): void { + public static function consume(HttpResponse $response): void { + error_log("consuming response" . $response->getCode()); http_response_code($response->getCode()); + header('Access-Control-Allow-Origin: *'); + header('Access-Control-Allow-Headers: *'); + foreach ($response->getHeaders() as $header => $value) { header("$header: $value"); } diff --git a/src/Api/Controller/APIAccountsController.php b/src/Api/Controller/APIAccountsController.php index 3fe2de0..3ec62df 100644 --- a/src/Api/Controller/APIAccountsController.php +++ b/src/Api/Controller/APIAccountsController.php @@ -10,16 +10,20 @@ use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Validation\DefaultValidators; +use IQBall\Core\Model\AuthModel; +use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\ValidationFail; class APIAccountsController { private AccountGateway $accounts; + private AuthModel $authModel; /** * @param AccountGateway $accounts */ - public function __construct(AccountGateway $accounts) { + public function __construct(AuthModel $model, AccountGateway $accounts) { $this->accounts = $accounts; + $this->authModel = $model; } @@ -33,12 +37,14 @@ class APIAccountsController { 'n' => [DefaultValidators::isUnsignedInteger()], ], function (HttpRequest $req) { $accounts = $this->accounts->listAccounts(intval($req['start']), intval($req['n'])); - $response = array_map(fn(Account $acc) => $acc->getUser(), $accounts); - return new JsonHttpResponse($response); + $users = array_map(fn(Account $acc) => $acc->getUser(), $accounts); + return new JsonHttpResponse([ + "users" => $users, + "totalCount" => $this->accounts->totalCount(), + ]); }, true); } - /** * @param int $userId * @return HttpResponse given user information. @@ -52,4 +58,49 @@ class APIAccountsController { return new JsonHttpResponse($acc->getUser()); } + + public function addUser(): HttpResponse { + return Control::runChecked([ + "username" => [DefaultValidators::name()], + "email" => [DefaultValidators::email()], + "password" => [DefaultValidators::password()], + "isAdmin" => [DefaultValidators::bool()], + ], function (HttpRequest $req) { + $model = new AuthModel($this->accounts); + + $account = $model->register($req["username"], $req["password"], $req["email"]); + if ($account == null) { + return new JsonHttpResponse([new ValidationFail("already exists", "An account with provided email ")], HttpCodes::FORBIDDEN); + } + + return new JsonHttpResponse([ + "id" => $account->getUser()->getId(), + ]); + }, true); + } + + public function removeUsers(): HttpResponse { + return Control::runChecked([ + "identifiers" => [DefaultValidators::array(), DefaultValidators::forall(DefaultValidators::isUnsignedInteger())], + ], function (HttpRequest $req) { + $this->accounts->removeAccounts($req["identifiers"]); + return HttpResponse::fromCode(HttpCodes::OK); + }, true); + } + + public function updateUser(int $id): HttpResponse { + return Control::runChecked([ + "email" => [DefaultValidators::email()], + "username" => [DefaultValidators::name()], + "isAdmin" => [DefaultValidators::bool()], + ], function (HttpRequest $req) use ($id) { + $mailAccount = $this->accounts->getAccountFromMail($req["email"]); + if ($mailAccount->getUser()->getId() != $id) { + return new JsonHttpResponse([new ValidationFail("email exists", "The provided mail address already exists for another account.")], HttpCodes::FORBIDDEN); + } + + $this->authModel->update($id, $req["email"], $req["username"], $req["isAdmin"]); + return HttpResponse::fromCode(HttpCodes::OK); + }, true); + } } diff --git a/src/Api/Controller/APIAuthController.php b/src/Api/Controller/APIAuthController.php index e257844..d21fea3 100644 --- a/src/Api/Controller/APIAuthController.php +++ b/src/Api/Controller/APIAuthController.php @@ -28,7 +28,7 @@ class APIAuthController { public function authorize(): HttpResponse { return Control::runChecked([ "email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)], - "password" => [DefaultValidators::lenBetween(6, 256)], + "password" => [DefaultValidators::password()], ], function (HttpRequest $req) { $failures = []; $account = $this->model->login($req["email"], $req["password"], $failures); diff --git a/src/App/Control.php b/src/App/Control.php index 0e517e4..7f04cbe 100644 --- a/src/App/Control.php +++ b/src/App/Control.php @@ -25,7 +25,7 @@ class Control { $fail = new ValidationFail("bad-payload", "request body is not a valid json object"); if ($errorInJson) { - return new JsonHttpResponse([$fail]); + return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST); } return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST); @@ -49,7 +49,7 @@ class Control { if (!empty($fails)) { if ($errorInJson) { - return new JsonHttpResponse($fails); + return new JsonHttpResponse($fails, HttpCodes::BAD_REQUEST); } return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST); } diff --git a/src/App/Controller/AuthController.php b/src/App/Controller/AuthController.php index 3d16733..efb8862 100644 --- a/src/App/Controller/AuthController.php +++ b/src/App/Controller/AuthController.php @@ -7,7 +7,9 @@ use IQBall\App\ViewHttpResponse; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Model\AuthModel; + use IQBall\Core\Validation\DefaultValidators; +use IQBall\Core\Validation\FieldValidationFail; class AuthController { private AuthModel $model; @@ -31,10 +33,10 @@ class AuthController { */ public function register(array $request, MutableSessionHandle $session): HttpResponse { $fails = []; - HttpRequest::from($request, $fails, [ + $request = HttpRequest::from($request, $fails, [ "username" => [DefaultValidators::name(), DefaultValidators::lenBetween(2, 32)], - "password" => [DefaultValidators::lenBetween(6, 256)], - "confirmpassword" => [DefaultValidators::lenBetween(6, 256)], + "password" => [DefaultValidators::password()], + "confirmpassword" => [DefaultValidators::password()], "email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)], ]); @@ -44,7 +46,16 @@ class AuthController { } } - $account = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email'], $fails); + if ($request["password"] != $request['confirmpassword']) { + $fails[] = new FieldValidationFail("confirmpassword", "Le mot de passe et la confirmation ne sont pas les mêmes."); + } + + $account = $this->model->register($request['username'], $request["password"], $request['email']); + + if (!$account) { + $fails[] = new FieldValidationFail("email", "L'email existe déjà"); + } + if (!empty($fails)) { return ViewHttpResponse::twig("display_register.html.twig", ['fails' => $fails]); } @@ -52,7 +63,7 @@ class AuthController { $target_url = $session->getInitialTarget(); if ($target_url != null) { - return HttpResponse::redirect_absolute($target_url); + return HttpResponse::redirectAbsolute($target_url); } return HttpResponse::redirect("/home"); @@ -81,7 +92,7 @@ class AuthController { $target_url = $session->getInitialTarget(); $session->setInitialTarget(null); if ($target_url != null) { - return HttpResponse::redirect_absolute($target_url); + return HttpResponse::redirectAbsolute($target_url); } return HttpResponse::redirect("/home"); diff --git a/src/Core/Gateway/AccountGateway.php b/src/Core/Gateway/AccountGateway.php index 6251b85..d10fb14 100644 --- a/src/Core/Gateway/AccountGateway.php +++ b/src/Core/Gateway/AccountGateway.php @@ -2,7 +2,6 @@ namespace IQBall\Core\Gateway; -use Cassandra\PreparedStatement; use IQBall\Core\Connection; use IQBall\Core\Data\Account; use IQBall\Core\Data\User; @@ -19,16 +18,26 @@ class AccountGateway { } public function insertAccount(string $name, string $email, string $token, string $hash, string $profilePicture): int { - $this->con->exec("INSERT INTO Account(username, hash, email, token,profile_picture) VALUES (:username,:hash,:email,:token,:profilePic)", [ + $this->con->exec("INSERT INTO Account(username, hash, email, token,profile_picture) VALUES (:username,:hash,:email,:token,:profile_pic)", [ ':username' => [$name, PDO::PARAM_STR], ':hash' => [$hash, PDO::PARAM_STR], ':email' => [$email, PDO::PARAM_STR], ':token' => [$token, PDO::PARAM_STR], - ':profilePic' => [$profilePicture, PDO::PARAM_STR], + ':profile_pic' => [$profilePicture, PDO::PARAM_STR], ]); return intval($this->con->lastInsertId()); } + public function updateAccount(int $id, string $name, string $email, string $token, bool $isAdmin): void { + $this->con->exec("UPDATE Account SET username = :username, email = :email, token = :token, is_admin = :is_admin WHERE id = :id", [ + ':username' => [$name, PDO::PARAM_STR], + ':email' => [$email, PDO::PARAM_STR], + ':token' => [$token, PDO::PARAM_STR], + ':id' => [$id, PDO::PARAM_INT], + ':is_admin' => [$isAdmin, PDO::PARAM_BOOL], + ]); + } + /** * promote or demote a user to server administrator @@ -122,7 +131,7 @@ class AccountGateway { * * @param integer $n the number of accounts to retrieve * @param int $start starting index of the list content - * @return Account[] + * @return Account[]|null */ public function listAccounts(int $start, int $n): ?array { $res = $this->con->fetch( @@ -132,7 +141,27 @@ class AccountGateway { ":n" => [$n, PDO::PARAM_INT], ] ); - return array_map(fn(array $acc) => new Account($acc["email"], new User($acc["username"], $acc["token"], $acc["id"], $acc["profile_picture"], $acc["is_admin"])), $res); + return array_map(fn(array $acc) => new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $acc["is_admin"])), $res); } + /** + * returns the total amount of accounts in the database + * @return int + */ + public function totalCount(): int { + return $this->con->fetch("SELECT count(*) FROM Account", [])[0]['count(*)']; + } + + /** + * remove a bunch of account identifiers + * @param int[] $accountIds + */ + public function removeAccounts(array $accountIds): void { + foreach ($accountIds as $accountId) { + $this->con->fetch("DELETE FROM Account WHERE id = :accountId", [ + ":accountId" => [$accountId, PDO::PARAM_INT], + ]); + } + + } } diff --git a/src/Core/Http/HttpResponse.php b/src/Core/Http/HttpResponse.php index c98a261..6c6a743 100644 --- a/src/Core/Http/HttpResponse.php +++ b/src/Core/Http/HttpResponse.php @@ -45,7 +45,7 @@ class HttpResponse { public static function redirect(string $url, int $code = HttpCodes::FOUND): HttpResponse { global $basePath; - return self::redirect_absolute($basePath . $url, $code); + return self::redirectAbsolute($basePath . $url, $code); } /** @@ -54,7 +54,7 @@ class HttpResponse { * @return HttpResponse a response that will redirect client to given url */ - public static function redirect_absolute(string $url, int $code = HttpCodes::FOUND): HttpResponse { + public static function redirectAbsolute(string $url, int $code = HttpCodes::FOUND): HttpResponse { if ($code < 300 || $code >= 400) { throw new \InvalidArgumentException("given code is not a redirection http code"); } diff --git a/src/Core/Model/AuthModel.php b/src/Core/Model/AuthModel.php index b937924..e1fc1bb 100644 --- a/src/Core/Model/AuthModel.php +++ b/src/Core/Model/AuthModel.php @@ -6,6 +6,8 @@ use Exception; use IQBall\Core\Data\Account; use IQBall\Core\Data\User; use IQBall\Core\Gateway\AccountGateway; +use IQBall\Core\Http\HttpCodes; +use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\ValidationFail; @@ -23,34 +25,19 @@ class AuthModel { /** * @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 - * @throws Exception + * @return Account|null the registered account or null if the account already exists for the given email address */ public function register( string $username, string $password, - string $confirmPassword, - string $email, - array &$failures + string $email ): ?Account { - - 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, self::DEFAULT_PROFILE_PICTURE); return new Account($token, new User($email, $username, $accountId, self::DEFAULT_PROFILE_PICTURE, false)); @@ -59,10 +46,13 @@ class AuthModel { /** * Generate a random base 64 string * @return string - * @throws Exception */ public static function generateToken(): string { - return base64_encode(random_bytes(64)); + try { + return base64_encode(random_bytes(64)); + } catch (Exception $e) { + throw new \RuntimeException($e); + } } /** @@ -80,4 +70,9 @@ class AuthModel { return $this->gateway->getAccountFromMail($email); } + public function update(int $id, string $email, string $username, bool $isAdmin): void { + $token = $this->generateToken(); + $this->gateway->updateAccount($id, $username, $email, $token, $isAdmin); + } + } diff --git a/src/Core/Validation/DefaultValidators.php b/src/Core/Validation/DefaultValidators.php index 67d5da2..6e64e30 100644 --- a/src/Core/Validation/DefaultValidators.php +++ b/src/Core/Validation/DefaultValidators.php @@ -38,6 +38,10 @@ class DefaultValidators { return self::regex("/^[0-9a-zA-Zà-üÀ-Ü _-]*$/"); } + public static function password(): Validator { + return self::lenBetween(6, 256); + } + /** * Validate string if its length is between given range * @param int $min minimum accepted length, inclusive @@ -82,10 +86,63 @@ class DefaultValidators { ); } + /** + * @param mixed[] $values + * @return Validator + */ + public static function oneOf(array $values): Validator { + return new SimpleFunctionValidator( + fn(string $val) => in_array($val, $values), + fn(string $name) => [new FieldValidationFail($name, "The value must be one of '" . join(", ", $values) . "'")] + ); + } + + public static function bool(): Validator { + return self::oneOf([true, false]); + } + public static function isURL(): Validator { return new SimpleFunctionValidator( fn($val) => filter_var($val, FILTER_VALIDATE_URL), fn(string $name) => [new FieldValidationFail($name, "The value is not an URL")] ); } + + /** + * @return Validator + */ + public static function array(): Validator { + return new SimpleFunctionValidator( + fn($val) => is_array($val), + fn(string $name) => [new FieldValidationFail($name, "The value is not an array")] + ); + } + + /** + * @param Validator $validator + * @return Validator + */ + public static function forall(Validator $validator): Validator { + return new class ($validator) extends Validator { + private Validator $validator; + + /** + * @param Validator $validator + */ + public function __construct(Validator $validator) { + $this->validator = $validator; + } + + public function validate(string $name, $val): array { + $failures = []; + $idx = 0; + foreach ($val as $item) { + $failures = array_merge($failures, $this->validator->validate($name . "[$idx]", $item)); + $idx += 1; + } + + return $failures; + } + }; + } } From 8e400f0dd83a09553923a8bc92cbbcd58064b589 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 17 Jan 2024 10:13:26 +0100 Subject: [PATCH 16/22] add search functionnality --- src/Api/Controller/APIAccountsController.php | 4 ++-- src/Core/Gateway/AccountGateway.php | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Api/Controller/APIAccountsController.php b/src/Api/Controller/APIAccountsController.php index 3ec62df..4ad3db7 100644 --- a/src/Api/Controller/APIAccountsController.php +++ b/src/Api/Controller/APIAccountsController.php @@ -11,7 +11,6 @@ use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Validation\DefaultValidators; use IQBall\Core\Model\AuthModel; -use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\ValidationFail; class APIAccountsController { @@ -35,8 +34,9 @@ class APIAccountsController { return Control::runCheckedFrom($request, [ 'start' => [DefaultValidators::isUnsignedInteger()], 'n' => [DefaultValidators::isUnsignedInteger()], + 'search' => [DefaultValidators::lenBetween(0, 256)], ], function (HttpRequest $req) { - $accounts = $this->accounts->listAccounts(intval($req['start']), intval($req['n'])); + $accounts = $this->accounts->searchAccounts(intval($req['start']), intval($req['n']), $req["search"]); $users = array_map(fn(Account $acc) => $acc->getUser(), $accounts); return new JsonHttpResponse([ "users" => $users, diff --git a/src/Core/Gateway/AccountGateway.php b/src/Core/Gateway/AccountGateway.php index d10fb14..44a8b58 100644 --- a/src/Core/Gateway/AccountGateway.php +++ b/src/Core/Gateway/AccountGateway.php @@ -133,12 +133,13 @@ class AccountGateway { * @param int $start starting index of the list content * @return Account[]|null */ - public function listAccounts(int $start, int $n): ?array { + public function searchAccounts(int $start, int $n, ?string $searchString): ?array { $res = $this->con->fetch( - "SELECT * FROM Account ORDER BY email LIMIT :offset, :n", + "SELECT * FROM Account WHERE username LIKE '%' || :search || '%' OR email LIKE '%' || :search || '%' ORDER BY username, email LIMIT :offset, :n", [ ":offset" => [$start, PDO::PARAM_INT], ":n" => [$n, PDO::PARAM_INT], + ":search" => [$searchString ?? "", PDO::PARAM_STR], ] ); return array_map(fn(array $acc) => new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $acc["is_admin"])), $res); From a7f5a715323beed1d43909f3a6fccbaaa1a6f4ff Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 17 Jan 2024 11:07:03 +0100 Subject: [PATCH 17/22] fix update user --- src/Api/API.php | 1 - src/Api/Controller/APIAccountsController.php | 2 +- src/Core/Gateway/AccountGateway.php | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Api/API.php b/src/Api/API.php index b955963..cc61c8d 100644 --- a/src/Api/API.php +++ b/src/Api/API.php @@ -12,7 +12,6 @@ use IQBall\Core\Validation\ValidationFail; class API { public static function consume(HttpResponse $response): void { - error_log("consuming response" . $response->getCode()); http_response_code($response->getCode()); header('Access-Control-Allow-Origin: *'); diff --git a/src/Api/Controller/APIAccountsController.php b/src/Api/Controller/APIAccountsController.php index 4ad3db7..1956b80 100644 --- a/src/Api/Controller/APIAccountsController.php +++ b/src/Api/Controller/APIAccountsController.php @@ -94,7 +94,7 @@ class APIAccountsController { "username" => [DefaultValidators::name()], "isAdmin" => [DefaultValidators::bool()], ], function (HttpRequest $req) use ($id) { - $mailAccount = $this->accounts->getAccountFromMail($req["email"]); + $mailAccount = $this->accounts->getAccount($id); if ($mailAccount->getUser()->getId() != $id) { return new JsonHttpResponse([new ValidationFail("email exists", "The provided mail address already exists for another account.")], HttpCodes::FORBIDDEN); } diff --git a/src/Core/Gateway/AccountGateway.php b/src/Core/Gateway/AccountGateway.php index 44a8b58..3c5070a 100644 --- a/src/Core/Gateway/AccountGateway.php +++ b/src/Core/Gateway/AccountGateway.php @@ -163,6 +163,5 @@ class AccountGateway { ":accountId" => [$accountId, PDO::PARAM_INT], ]); } - } } From bfb216bfafa41bb8eb441de59e7636e10f50cd89 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 17 Jan 2024 16:05:17 +0100 Subject: [PATCH 18/22] fix suggestions --- Documentation/database_mld.puml | 4 +- sql/setup-tables.sql | 9 +++- src/Api/APIControl.php | 44 +++++++++++++++++++ src/Api/Controller/APIAccountsController.php | 20 +++++---- src/Api/Controller/APIAuthController.php | 5 ++- src/Api/Controller/APITacticController.php | 12 ++--- src/App/App.php | 1 - src/App/AppControl.php | 44 +++++++++++++++++++ src/App/Controller/UserController.php | 1 + src/{App => Core}/Control.php | 26 ++++------- .../ControlSchemaErrorResponseFactory.php | 14 ++++++ src/Core/Gateway/AccountGateway.php | 26 ++++++++--- 12 files changed, 160 insertions(+), 46 deletions(-) create mode 100644 src/Api/APIControl.php create mode 100644 src/App/AppControl.php rename src/{App => Core}/Control.php (67%) create mode 100644 src/Core/ControlSchemaErrorResponseFactory.php diff --git a/Documentation/database_mld.puml b/Documentation/database_mld.puml index 170fad7..8b4d32f 100644 --- a/Documentation/database_mld.puml +++ b/Documentation/database_mld.puml @@ -5,8 +5,8 @@ object Account { name age email - phoneNumber - passwordHash + phone_number + password_hash profile_picture } diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 1a756f6..77f2b3d 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -4,6 +4,12 @@ DROP TABLE IF EXISTS Tactic; DROP TABLE IF EXISTS Team; DROP TABLE IF EXISTS User; DROP TABLE IF EXISTS Member; + +CREATE TABLE Admins +( + id integer PRIMARY KEY REFERENCES Account +); + CREATE TABLE Account ( id integer PRIMARY KEY AUTOINCREMENT, @@ -11,8 +17,7 @@ CREATE TABLE Account username varchar NOT NULL, token varchar UNIQUE NOT NULL, hash varchar NOT NULL, - profile_picture varchar NOT NULL, - is_admin boolean DEFAULT false NOT NULL + profile_picture varchar NOT NULL ); CREATE TABLE Tactic diff --git a/src/Api/APIControl.php b/src/Api/APIControl.php new file mode 100644 index 0000000..260575a --- /dev/null +++ b/src/Api/APIControl.php @@ -0,0 +1,44 @@ + $schema an array of `fieldName => DefaultValidators` which represents the request object schema + * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. + * The callback must accept an HttpRequest, and return an HttpResponse object. + * @return HttpResponse + */ + public static function runChecked(array $schema, callable $run): HttpResponse { + return Control::runChecked($schema, $run, self::errorFactory()); + } + + /** + * Runs given callback, if the given request data array validates the given schema. + * @param array $data the request's data array. + * @param array $schema an array of `fieldName => DefaultValidators` which represents the request object schema + * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. + * The callback must accept an HttpRequest, and return an HttpResponse object. + * @return HttpResponse + */ + public static function runCheckedFrom(array $data, array $schema, callable $run): HttpResponse { + return Control::runCheckedFrom($data, $schema, $run, self::errorFactory()); + } + +} diff --git a/src/Api/Controller/APIAccountsController.php b/src/Api/Controller/APIAccountsController.php index 1956b80..13d9db4 100644 --- a/src/Api/Controller/APIAccountsController.php +++ b/src/Api/Controller/APIAccountsController.php @@ -2,6 +2,7 @@ namespace IQBall\Api\Controller; +use IQBall\Api\APIControl; use IQBall\App\Control; use IQBall\Core\Data\Account; use IQBall\Core\Gateway\AccountGateway; @@ -9,8 +10,8 @@ use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; -use IQBall\Core\Validation\DefaultValidators; use IQBall\Core\Model\AuthModel; +use IQBall\Core\Validation\DefaultValidators; use IQBall\Core\Validation\ValidationFail; class APIAccountsController { @@ -18,6 +19,7 @@ class APIAccountsController { private AuthModel $authModel; /** + * @param AuthModel $model * @param AccountGateway $accounts */ public function __construct(AuthModel $model, AccountGateway $accounts) { @@ -31,7 +33,7 @@ class APIAccountsController { * @return HttpResponse */ public function listUsers(array $request): HttpResponse { - return Control::runCheckedFrom($request, [ + return APIControl::runCheckedFrom($request, [ 'start' => [DefaultValidators::isUnsignedInteger()], 'n' => [DefaultValidators::isUnsignedInteger()], 'search' => [DefaultValidators::lenBetween(0, 256)], @@ -42,7 +44,7 @@ class APIAccountsController { "users" => $users, "totalCount" => $this->accounts->totalCount(), ]); - }, true); + }); } /** @@ -60,7 +62,7 @@ class APIAccountsController { } public function addUser(): HttpResponse { - return Control::runChecked([ + return APIControl::runChecked([ "username" => [DefaultValidators::name()], "email" => [DefaultValidators::email()], "password" => [DefaultValidators::password()], @@ -76,20 +78,20 @@ class APIAccountsController { return new JsonHttpResponse([ "id" => $account->getUser()->getId(), ]); - }, true); + }); } public function removeUsers(): HttpResponse { - return Control::runChecked([ + return APIControl::runChecked([ "identifiers" => [DefaultValidators::array(), DefaultValidators::forall(DefaultValidators::isUnsignedInteger())], ], function (HttpRequest $req) { $this->accounts->removeAccounts($req["identifiers"]); return HttpResponse::fromCode(HttpCodes::OK); - }, true); + }); } public function updateUser(int $id): HttpResponse { - return Control::runChecked([ + return APIControl::runChecked([ "email" => [DefaultValidators::email()], "username" => [DefaultValidators::name()], "isAdmin" => [DefaultValidators::bool()], @@ -101,6 +103,6 @@ class APIAccountsController { $this->authModel->update($id, $req["email"], $req["username"], $req["isAdmin"]); return HttpResponse::fromCode(HttpCodes::OK); - }, true); + }); } } diff --git a/src/Api/Controller/APIAuthController.php b/src/Api/Controller/APIAuthController.php index d21fea3..c715803 100644 --- a/src/Api/Controller/APIAuthController.php +++ b/src/Api/Controller/APIAuthController.php @@ -2,6 +2,7 @@ namespace IQBall\Api\Controller; +use IQBall\Api\APIControl; use IQBall\App\Control; use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpRequest; @@ -26,7 +27,7 @@ class APIAuthController { * @return HttpResponse */ public function authorize(): HttpResponse { - return Control::runChecked([ + return APIControl::runChecked([ "email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)], "password" => [DefaultValidators::password()], ], function (HttpRequest $req) { @@ -38,6 +39,6 @@ class APIAuthController { } return new JsonHttpResponse(["authorization" => $account->getToken()]); - }, true); + }); } } diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 0d99f41..9f71212 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -2,7 +2,7 @@ namespace IQBall\Api\Controller; -use IQBall\App\Control; +use IQBall\Api\APIControl; use IQBall\Core\Data\Account; use IQBall\Core\Data\TacticInfo; use IQBall\Core\Http\HttpCodes; @@ -32,7 +32,7 @@ class APITacticController { * @return HttpResponse */ public function updateName(int $tactic_id, Account $account): HttpResponse { - return Control::runChecked([ + return APIControl::runChecked([ "name" => [DefaultValidators::lenBetween(1, 50), DefaultValidators::nameWithSpaces()], ], function (HttpRequest $request) use ($tactic_id, $account) { @@ -44,23 +44,23 @@ class APITacticController { } return HttpResponse::fromCode(HttpCodes::OK); - }, true); + }); } /** * @param int $id - * @param Account $account * @return HttpResponse */ public function saveContent(int $id, Account $account): HttpResponse { - return Control::runChecked([ + return APIControl::runChecked([ "content" => [], ], function (HttpRequest $req) use ($id) { + //TODO verify that the account has the rights to update the tactic content if ($fail = $this->model->updateContent($id, json_encode($req["content"]))) { return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST); } return HttpResponse::fromCode(HttpCodes::OK); - }, true); + }); } diff --git a/src/App/App.php b/src/App/App.php index cba59de..5f208bc 100644 --- a/src/App/App.php +++ b/src/App/App.php @@ -90,7 +90,6 @@ class App { if ($action->getAuthType() == Action::AUTH_ADMIN && !$account->getUser()->isAdmin()) { return new JsonHttpResponse([ValidationFail::unauthorized()], HttpCodes::UNAUTHORIZED); } - } return $action->run($params, $session); diff --git a/src/App/AppControl.php b/src/App/AppControl.php new file mode 100644 index 0000000..c313e69 --- /dev/null +++ b/src/App/AppControl.php @@ -0,0 +1,44 @@ + $failures], HttpCodes::BAD_REQUEST); + } + }; + } + + /** + * Runs given callback, if the request's payload json validates the given schema. + * @param array $schema an array of `fieldName => DefaultValidators` which represents the request object schema + * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. + * The callback must accept an HttpRequest, and return an HttpResponse object. + * @return HttpResponse + */ + public static function runChecked(array $schema, callable $run): HttpResponse { + return Control::runChecked($schema, $run, self::errorFactory()); + } + + /** + * Runs given callback, if the given request data array validates the given schema. + * @param array $data the request's data array. + * @param array $schema an array of `fieldName => DefaultValidators` which represents the request object schema + * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. + * The callback must accept an HttpRequest, and return an HttpResponse object. + * @return HttpResponse + */ + public static function runCheckedFrom(array $data, array $schema, callable $run): HttpResponse { + return Control::runCheckedFrom($data, $schema, $run, self::errorFactory()); + } + +} diff --git a/src/App/Controller/UserController.php b/src/App/Controller/UserController.php index 6f56128..616bf54 100644 --- a/src/App/Controller/UserController.php +++ b/src/App/Controller/UserController.php @@ -5,6 +5,7 @@ namespace IQBall\App\Controller; use IQBall\App\Session\MutableSessionHandle; use IQBall\App\Session\SessionHandle; use IQBall\App\ViewHttpResponse; +use IQBall\Core\Gateway\AccountGateway; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TeamModel; diff --git a/src/App/Control.php b/src/Core/Control.php similarity index 67% rename from src/App/Control.php rename to src/Core/Control.php index 7f04cbe..106052d 100644 --- a/src/App/Control.php +++ b/src/Core/Control.php @@ -1,6 +1,6 @@ $schema an array of `fieldName => DefaultValidators` which represents the request object schema * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. * The callback must accept an HttpRequest, and return an HttpResponse object. - * @param bool $errorInJson if set to true, the returned response will be a JsonHttpResponse if the schema isn't validated + * @param ControlSchemaErrorResponseFactory $errorFactory an error factory to use if the request does not validate the required schema * @return HttpResponse */ - public static function runChecked(array $schema, callable $run, bool $errorInJson = false): HttpResponse { + public static function runChecked(array $schema, callable $run, ControlSchemaErrorResponseFactory $errorFactory): HttpResponse { $request_body = file_get_contents('php://input'); $payload_obj = json_decode($request_body); if (!$payload_obj instanceof \stdClass) { $fail = new ValidationFail("bad-payload", "request body is not a valid json object"); - - if ($errorInJson) { - return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST); - } - - return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST); + return $errorFactory->apply([$fail]); } $payload = get_object_vars($payload_obj); - return self::runCheckedFrom($payload, $schema, $run, $errorInJson); + return self::runCheckedFrom($payload, $schema, $run, $errorFactory); } /** @@ -40,18 +35,15 @@ class Control { * @param array $schema an array of `fieldName => DefaultValidators` which represents the request object schema * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. * The callback must accept an HttpRequest, and return an HttpResponse object. - * @param bool $errorInJson if set to true, the returned response will be a JsonHttpResponse if the schema isn't validated + * @param ControlSchemaErrorResponseFactory $errorFactory an error factory to use if the request does not validate the required schema * @return HttpResponse */ - public static function runCheckedFrom(array $data, array $schema, callable $run, bool $errorInJson = false): HttpResponse { + public static function runCheckedFrom(array $data, array $schema, callable $run, ControlSchemaErrorResponseFactory $errorFactory): HttpResponse { $fails = []; $request = HttpRequest::from($data, $fails, $schema); if (!empty($fails)) { - if ($errorInJson) { - return new JsonHttpResponse($fails, HttpCodes::BAD_REQUEST); - } - return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST); + return $errorFactory->apply($fails); } return call_user_func_array($run, [$request]); diff --git a/src/Core/ControlSchemaErrorResponseFactory.php b/src/Core/ControlSchemaErrorResponseFactory.php new file mode 100644 index 0000000..9882a65 --- /dev/null +++ b/src/Core/ControlSchemaErrorResponseFactory.php @@ -0,0 +1,14 @@ +con->exec("UPDATE Account SET username = :username, email = :email, token = :token, is_admin = :is_admin WHERE id = :id", [ + $this->con->exec("UPDATE Account SET username = :username, email = :email, token = :token WHERE id = :id", [ ':username' => [$name, PDO::PARAM_STR], ':email' => [$email, PDO::PARAM_STR], ':token' => [$token, PDO::PARAM_STR], ':id' => [$id, PDO::PARAM_INT], - ':is_admin' => [$isAdmin, PDO::PARAM_BOOL], ]); + $this->setIsAdmin($id, $isAdmin); + } + + public function isAdmin(int $id): bool { + $stmnt = $this->con->prepare("SELECT * FROM Admins WHERE id = :id"); + $stmnt->bindValue(':id', $id, PDO::PARAM_INT); + $stmnt->execute(); + $result = $stmnt->fetchAll(PDO::FETCH_ASSOC); + + return !empty($result); } @@ -46,8 +55,11 @@ class AccountGateway { * @return bool true if the given user exists */ public function setIsAdmin(int $id, bool $isAdmin): bool { - $stmnt = $this->con->prepare("UPDATE Account SET is_admin = :is_admin WHERE id = :id"); - $stmnt->bindValue(':is_admin', $isAdmin); + if ($isAdmin) { + $stmnt = $this->con->prepare("INSERT INTO Admins VALUES(:id)"); + } else { + $stmnt = $this->con->prepare("DELETE FROM Admins WHERE id = :id"); + } $stmnt->bindValue(':id', $id); $stmnt->execute(); @@ -92,7 +104,7 @@ class AccountGateway { return null; } - return new Account($acc["token"], new User($email, $acc["username"], $acc["id"], $acc["profile_picture"], $acc['is_admin'])); + return new Account($acc["token"], new User($email, $acc["username"], $acc["id"], $acc["profile_picture"], $this->isAdmin($acc["id"]))); } /** @@ -123,7 +135,7 @@ class AccountGateway { return null; } - return new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $acc["is_admin"])); + return new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $this->isAdmin($acc["id"]))); } /** @@ -142,7 +154,7 @@ class AccountGateway { ":search" => [$searchString ?? "", PDO::PARAM_STR], ] ); - return array_map(fn(array $acc) => new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $acc["is_admin"])), $res); + return array_map(fn(array $acc) => new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $this->isAdmin($acc["id"]))), $res); } /** From 2ef68eacf1190fc70cdd64cec3e9e52d9b47d0e1 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 17 Jan 2024 17:37:43 +0100 Subject: [PATCH 19/22] apply suggestions --- config.php | 6 ++++++ profiles/dev-config-profile.php | 16 ++++++++++++++++ profiles/prod-config-profile.php | 3 +++ sql/database.php | 17 ----------------- src/Api/APIControl.php | 3 ++- src/Api/Controller/APIAccountsController.php | 2 +- src/Core/Gateway/AccountGateway.php | 4 ++-- src/Core/Model/TacticModel.php | 4 ++-- src/Core/Validation/DefaultValidators.php | 6 ++---- 9 files changed, 34 insertions(+), 27 deletions(-) diff --git a/config.php b/config.php index fdf02a4..0dd030a 100644 --- a/config.php +++ b/config.php @@ -3,6 +3,7 @@ // `dev-config-profile.php` by default. // on production server the included profile is `prod-config-profile.php`. // Please do not touch. + require /*PROFILE_FILE*/ "profiles/dev-config-profile.php"; const SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH; @@ -21,3 +22,8 @@ global $_data_source_name; $data_source_name = $_data_source_name; const DATABASE_USER = _DATABASE_USER; const DATABASE_PASSWORD = _DATABASE_PASSWORD; + + +function init_database(PDO $pdo): void { + _init_database($pdo); +} diff --git a/profiles/dev-config-profile.php b/profiles/dev-config-profile.php index bd87f1d..e39f2f0 100644 --- a/profiles/dev-config-profile.php +++ b/profiles/dev-config-profile.php @@ -1,5 +1,9 @@ insertAccount($name, $email, AuthModel::generateToken(), password_hash("123456", PASSWORD_DEFAULT), "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png"); + $accounts->setIsAdmin($id, true); + } +} diff --git a/profiles/prod-config-profile.php b/profiles/prod-config-profile.php index e9bb12c..185541a 100644 --- a/profiles/prod-config-profile.php +++ b/profiles/prod-config-profile.php @@ -20,3 +20,6 @@ function _asset(string $assetURI): string { // fallback to the uri itself. return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI); } + + +function _init_database(PDO $pdo): void {} diff --git a/sql/database.php b/sql/database.php index af94ef2..69b53e7 100644 --- a/sql/database.php +++ b/sql/database.php @@ -1,9 +1,5 @@ insertAccount($name, $email, AuthModel::generateToken(), password_hash("123456", PASSWORD_DEFAULT), "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png"); - $accounts->setIsAdmin($id, true); - } -} diff --git a/src/Api/APIControl.php b/src/Api/APIControl.php index 260575a..751fbfb 100644 --- a/src/Api/APIControl.php +++ b/src/Api/APIControl.php @@ -4,6 +4,7 @@ namespace IQBall\Api; use IQBall\Core\Control; use IQBall\Core\ControlSchemaErrorResponseFactory; +use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; @@ -13,7 +14,7 @@ class APIControl { private static function errorFactory(): ControlSchemaErrorResponseFactory { return new class () implements ControlSchemaErrorResponseFactory { public function apply(array $failures): HttpResponse { - return new JsonHttpResponse($failures); + return new JsonHttpResponse($failures, HttpCodes::BAD_REQUEST); } }; } diff --git a/src/Api/Controller/APIAccountsController.php b/src/Api/Controller/APIAccountsController.php index 13d9db4..32fd956 100644 --- a/src/Api/Controller/APIAccountsController.php +++ b/src/Api/Controller/APIAccountsController.php @@ -35,7 +35,7 @@ class APIAccountsController { public function listUsers(array $request): HttpResponse { return APIControl::runCheckedFrom($request, [ 'start' => [DefaultValidators::isUnsignedInteger()], - 'n' => [DefaultValidators::isUnsignedInteger()], + 'n' => [DefaultValidators::isIntInRange(0, 250)], 'search' => [DefaultValidators::lenBetween(0, 256)], ], function (HttpRequest $req) { $accounts = $this->accounts->searchAccounts(intval($req['start']), intval($req['n']), $req["search"]); diff --git a/src/Core/Gateway/AccountGateway.php b/src/Core/Gateway/AccountGateway.php index d6b5686..6752b01 100644 --- a/src/Core/Gateway/AccountGateway.php +++ b/src/Core/Gateway/AccountGateway.php @@ -143,9 +143,9 @@ class AccountGateway { * * @param integer $n the number of accounts to retrieve * @param int $start starting index of the list content - * @return Account[]|null + * @return Account[] */ - public function searchAccounts(int $start, int $n, ?string $searchString): ?array { + public function searchAccounts(int $start, int $n, ?string $searchString): array { $res = $this->con->fetch( "SELECT * FROM Account WHERE username LIKE '%' || :search || '%' OR email LIKE '%' || :search || '%' ORDER BY username, email LIMIT :offset, :n", [ diff --git a/src/Core/Model/TacticModel.php b/src/Core/Model/TacticModel.php index 590a106..920075b 100644 --- a/src/Core/Model/TacticModel.php +++ b/src/Core/Model/TacticModel.php @@ -68,9 +68,9 @@ class TacticModel { * NOTE: if given user id does not match any user, this function returns an empty array * * @param integer $user_id - * @return TacticInfo[] | null + * @return TacticInfo[] */ - public function listAllOf(int $user_id): ?array { + public function listAllOf(int $user_id): array { return$this->tactics->listAllOf($user_id); } diff --git a/src/Core/Validation/DefaultValidators.php b/src/Core/Validation/DefaultValidators.php index 6e64e30..c898170 100644 --- a/src/Core/Validation/DefaultValidators.php +++ b/src/Core/Validation/DefaultValidators.php @@ -72,7 +72,7 @@ class DefaultValidators { public static function isInteger(): Validator { - return self::regex("/^-[0-9]+$/", "field is not an integer"); + return self::regex("/^[-+]?[0-9]+$/", "field is not an integer"); } public static function isUnsignedInteger(): Validator { @@ -135,10 +135,8 @@ class DefaultValidators { public function validate(string $name, $val): array { $failures = []; - $idx = 0; - foreach ($val as $item) { + foreach ($val as $idx => $item) { $failures = array_merge($failures, $this->validator->validate($name . "[$idx]", $item)); - $idx += 1; } return $failures; From 5aa1d1fbee9a4b0fc056e1c09fa6dd539ec85210 Mon Sep 17 00:00:00 2001 From: maxime Date: Sat, 20 Jan 2024 19:18:53 +0100 Subject: [PATCH 20/22] clarify documentation --- Documentation/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Documentation/README.md b/Documentation/README.md index dfc91c7..24c5472 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -1,3 +1,3 @@ -# The wiki also exists - -Some of our explanation are contained in the [wiki](https://codefirst.iut.uca.fr/git/IQBall/Application-Web/wiki) \ No newline at end of file +* [Description.md](Description.md) +* [Conception.md](Conception.md) +* [how-to-dev.md](how-to-dev.md) \ No newline at end of file From 87e73a3c5fa5281da2791247bce1a45bbb389838 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Mon, 8 Jan 2024 11:06:52 +0100 Subject: [PATCH 21/22] add manual deployment scripts --- .gitignore | 4 +++- ci/.drone.yml | 6 ++++-- ci/build_and_deploy_to.sh | 17 +++++++++++++++++ ci/build_react.msh | 15 ++++++--------- config.php | 4 ++++ front/views/Editor.tsx | 10 +--------- profiles/dev-config-profile.php | 4 ++++ profiles/prod-config-profile.php | 7 +++++++ public/.htaccess | 4 ++++ public/api/.htaccess | 4 ++++ public/api/index.php | 6 +++--- public/index.php | 3 +-- src/Api/API.php | 2 +- src/Api/Controller/APITacticController.php | 1 + src/App/App.php | 2 +- src/index-utils.php | 21 --------------------- 16 files changed, 61 insertions(+), 49 deletions(-) create mode 100755 ci/build_and_deploy_to.sh create mode 100644 public/.htaccess create mode 100644 public/api/.htaccess delete mode 100644 src/index-utils.php diff --git a/.gitignore b/.gitignore index 3934c5c..e5a863d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,13 +8,15 @@ vendor .nfs* composer.lock *.phar -/dist +dist .guard +outputs # sqlite database files *.sqlite views-mappings.php +.env.PROD # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. diff --git a/ci/.drone.yml b/ci/.drone.yml index a282766..42b1c42 100644 --- a/ci/.drone.yml +++ b/ci/.drone.yml @@ -37,8 +37,9 @@ steps: - echo n | /tmp/moshell_setup.sh - echo "VITE_API_ENDPOINT=/IQBall/$DRONE_BRANCH/public/api" >> .env.PROD - echo "VITE_BASE=/IQBall/$DRONE_BRANCH/public" >> .env.PROD + - apt update && apt install jq -y - - - /root/.local/bin/moshell ci/build_react.msh + - BASE="/IQBall/$DRONE_BRANCH/public" OUTPUT=/outputs /root/.local/bin/moshell ci/build_react.msh - image: ubuntu:latest name: "prepare php" @@ -48,7 +49,8 @@ steps: commands: - mkdir -p /outputs/public # this sed command will replace the included `profile/dev-config-profile.php` to `profile/prod-config-file.php` in the config.php file. - - sed -iE 's/\\/\\*PROFILE_FILE\\*\\/\\s*".*"/"profiles\\/prod-config-profile.php"/' config.php + - sed -E -i 's/\\/\\*PROFILE_FILE\\*\\/\\s*".*"/"profiles\\/prod-config-profile.php"/' config.php + - sed -E -i "s/const BASE_PATH = .*;/const BASE_PATH = \"\\/IQBall\\/$DRONE_BRANCH\\/public\";/" profiles/prod-config-profile.php - rm profiles/dev-config-profile.php - mv src config.php sql profiles vendor /outputs/ diff --git a/ci/build_and_deploy_to.sh b/ci/build_and_deploy_to.sh new file mode 100755 index 0000000..b0d2924 --- /dev/null +++ b/ci/build_and_deploy_to.sh @@ -0,0 +1,17 @@ + +export OUTPUT=$1 +export BASE=$2 + +rm -rf $OUTPUT/* + +echo "VITE_API_ENDPOINT=$BASE/api" >> .env.PROD +echo "VITE_BASE=$BASE" >> .env.PROD + +ci/build_react.msh + +mkdir -p $OUTPUT/profiles/ + +sed -E 's/\/\*PROFILE_FILE\*\/\s*".*"/"profiles\/prod-config-profile.php"/' config.php > $OUTPUT/config.php +sed -E "s/const BASE_PATH = .*;/const BASE_PATH = \"$(sed s/\\//\\\\\\//g <<< "$BASE")\";/" profiles/prod-config-profile.php > $OUTPUT/profiles/prod-config-profile.php + +cp -r vendor sql src public $OUTPUT diff --git a/ci/build_react.msh b/ci/build_react.msh index 3d3a8f0..a687ac6 100755 --- a/ci/build_react.msh +++ b/ci/build_react.msh @@ -1,20 +1,17 @@ #!/usr/bin/env moshell -mkdir -p /outputs/public +val base = std::env("BASE").unwrap() +val outputs = std::env("OUTPUT").unwrap() -apt update && apt install jq -y +mkdir -p $outputs/public -val drone_branch = std::env("DRONE_BRANCH").unwrap() - -val base = "/IQBall/$drone_branch/public" npm run build -- --base=$base --mode PROD // Read generated mappings from build val result = $(jq -r 'to_entries|map(.key + " " +.value.file)|.[]' dist/manifest.json) val mappings = $result.split('\n') - -echo ' views-mappings.php +echo ' views-mappings.php while $mappings.len() > 0 { val mapping = $mappings.pop().unwrap(); @@ -28,5 +25,5 @@ echo "];" >> views-mappings.php chmod +r views-mappings.php -mv dist/* front/assets/ front/style/ public/* /outputs/public/ -mv views-mappings.php /outputs/ +cp -r dist/* front/assets/ front/style/ public/* $outputs/public/ +cp -r views-mappings.php $outputs/ diff --git a/config.php b/config.php index 0dd030a..a3871c6 100644 --- a/config.php +++ b/config.php @@ -18,6 +18,10 @@ function asset(string $assetURI): string { return _asset($assetURI); } +function get_base_path(): string { + return _get_base_path(); +} + global $_data_source_name; $data_source_name = $_data_source_name; const DATABASE_USER = _DATABASE_USER; diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index cbb2da5..7dfea4f 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -121,15 +121,7 @@ function EditorView({ const [content, setContent, saveState] = useContentState( initialContent, isInGuestMode ? SaveStates.Guest : SaveStates.Ok, - useMemo( - () => - debounceAsync( - (content) => - onContentChange(content).then((success) => - success ? SaveStates.Ok : SaveStates.Err, - ), - 250, - ), + useMemo(() => debounceAsync(onContentChange), [onContentChange], ), ) diff --git a/profiles/dev-config-profile.php b/profiles/dev-config-profile.php index e39f2f0..0c06513 100644 --- a/profiles/dev-config-profile.php +++ b/profiles/dev-config-profile.php @@ -30,3 +30,7 @@ function _init_database(PDO $pdo): void { $accounts->setIsAdmin($id, true); } } + +function _get_base_path(): string { + return ""; +} diff --git a/profiles/prod-config-profile.php b/profiles/prod-config-profile.php index 185541a..224f8de 100644 --- a/profiles/prod-config-profile.php +++ b/profiles/prod-config-profile.php @@ -4,6 +4,9 @@ // in an `ASSETS` array constant. require __DIR__ . "/../views-mappings.php"; +// THIS VALUE IS TO SET IN THE CI +const BASE_PATH = null; + const _SUPPORTS_FAST_REFRESH = false; $database_file = __DIR__ . "/../database.sqlite"; $_data_source_name = "sqlite:/$database_file"; @@ -23,3 +26,7 @@ function _asset(string $assetURI): string { function _init_database(PDO $pdo): void {} + +function _get_base_path(): string { + return BASE_PATH; +} diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..36a836b --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,4 @@ +RewriteEngine on +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^.*$ ./index.php [NC,L,QSA] \ No newline at end of file diff --git a/public/api/.htaccess b/public/api/.htaccess new file mode 100644 index 0000000..36a836b --- /dev/null +++ b/public/api/.htaccess @@ -0,0 +1,4 @@ +RewriteEngine on +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^.*$ ./index.php [NC,L,QSA] \ No newline at end of file diff --git a/public/api/index.php b/public/api/index.php index 226e8f1..db14194 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -3,7 +3,6 @@ require "../../config.php"; require "../../vendor/autoload.php"; require "../../sql/database.php"; -require "../../src/index-utils.php"; use IQBall\Api\API; use IQBall\Api\Controller\APIAccountsController; @@ -19,7 +18,8 @@ use IQBall\Core\Gateway\TacticInfoGateway; use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\TacticModel; -$basePath = get_public_path(__DIR__); + +$basePath = get_base_path() . "/api"; function getTacticController(): APITacticController { return new APITacticController(new TacticModel(new TacticInfoGateway(new Connection(get_database())))); @@ -41,8 +41,8 @@ function getServerController(): APIServerController { } function getRoutes(): AltoRouter { - $router = new AltoRouter(); global $basePath; + $router = new AltoRouter(); $router->setBasePath($basePath); $router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize())); diff --git a/public/index.php b/public/index.php index 82dd37f..5d93c4c 100644 --- a/public/index.php +++ b/public/index.php @@ -4,7 +4,6 @@ require "../vendor/autoload.php"; require "../config.php"; require "../sql/database.php"; require "../src/App/react-display.php"; -require "../src/index-utils.php"; use IQBall\App\App; use IQBall\App\Controller\AuthController; @@ -125,6 +124,6 @@ function runMatch($match, MutableSessionHandle $session): HttpResponse { } //this is a global variable -$basePath = get_public_path(__DIR__); +$basePath = get_base_path(); App::render(runMatch(getRoutes()->match(), PhpSessionHandle::init()), fn() => getTwig()); diff --git a/src/Api/API.php b/src/Api/API.php index cc61c8d..143a838 100644 --- a/src/Api/API.php +++ b/src/Api/API.php @@ -38,7 +38,7 @@ class API { */ public static function handleMatch($match, callable $tryGetAuthorization): HttpResponse { if (!$match) { - return new JsonHttpResponse([ValidationFail::notFound("not found")]); + return new JsonHttpResponse([ValidationFail::notFound("not found")], HttpCodes::NOT_FOUND); } $action = $match['target']; diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 9f71212..3cfe02e 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -49,6 +49,7 @@ class APITacticController { /** * @param int $id + * @param Account $account * @return HttpResponse */ public function saveContent(int $id, Account $account): HttpResponse { diff --git a/src/App/App.php b/src/App/App.php index 5f208bc..1cfe6d7 100644 --- a/src/App/App.php +++ b/src/App/App.php @@ -84,7 +84,7 @@ class App { if ($account == null) { // put in the session the initial url the user wanted to get $session->setInitialTarget($_SERVER['REQUEST_URI']); - return HttpResponse::redirect($authRoute); + return HttpResponse::redirectAbsolute($authRoute); } if ($action->getAuthType() == Action::AUTH_ADMIN && !$account->getUser()->isAdmin()) { diff --git a/src/index-utils.php b/src/index-utils.php deleted file mode 100644 index eb600bc..0000000 --- a/src/index-utils.php +++ /dev/null @@ -1,21 +0,0 @@ - Date: Wed, 31 Jan 2024 19:31:43 +0100 Subject: [PATCH 22/22] Add Administration API for teams's administration panel (#97) Co-authored-by: Override-6 Co-authored-by: samuel Co-authored-by: sam Co-authored-by: sam Reviewed-on: https://codefirst.iut.uca.fr/git/IQBall/Application-Web/pulls/97 Co-authored-by: Samuel BERION Co-committed-by: Samuel BERION --- profiles/dev-config-profile.php | 7 ++ public/api/index.php | 15 +++- sql/database.php | 4 + src/Api/API.php | 1 + src/Api/Controller/APIAccountsController.php | 3 +- src/Api/Controller/APITacticController.php | 1 + src/Api/Controller/APITeamController.php | 79 ++++++++++++++++++++ src/App/App.php | 2 +- src/Core/Control.php | 2 + src/Core/Data/User.php | 2 +- src/Core/Gateway/AccountGateway.php | 3 +- src/Core/Gateway/MemberGateway.php | 1 + src/Core/Gateway/TeamGateway.php | 45 +++++++++++ src/Core/Model/TeamModel.php | 32 ++++++-- 14 files changed, 187 insertions(+), 10 deletions(-) create mode 100644 src/Api/Controller/APITeamController.php diff --git a/profiles/dev-config-profile.php b/profiles/dev-config-profile.php index 0c06513..b8a50be 100644 --- a/profiles/dev-config-profile.php +++ b/profiles/dev-config-profile.php @@ -21,14 +21,21 @@ function _asset(string $assetURI): string { function _init_database(PDO $pdo): void { $accounts = new AccountGateway(new Connection($pdo)); + $teams = new \IQBall\Core\Gateway\TeamGateway((new Connection($pdo))); $defaultAccounts = ["maxime", "mael", "yanis", "vivien"]; + $defaultTeams = ["Lakers", "Celtics", "Bulls"]; + foreach ($defaultAccounts as $name) { $email = "$name@mail.com"; $id = $accounts->insertAccount($name, $email, AuthModel::generateToken(), password_hash("123456", PASSWORD_DEFAULT), "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png"); $accounts->setIsAdmin($id, true); } + + foreach ($defaultTeams as $name) { + $id = $teams->insert($name, "https://lebasketographe.fr/wp-content/uploads/2019/11/nom-equipes-nba.jpg", "#1a2b3c", "#FF00AA"); + } } function _get_base_path(): string { diff --git a/public/api/index.php b/public/api/index.php index db14194..be2e8a3 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -33,6 +33,7 @@ function getAccountController(): APIAccountsController { $con = new Connection(get_database()); $gw = new AccountGateway($con); return new APIAccountsController(new AuthModel($gw), $gw); + } function getServerController(): APIServerController { @@ -40,6 +41,13 @@ function getServerController(): APIServerController { return new APIServerController($basePath, get_database()); } +function getAPITeamController(): \IQBall\Api\Controller\APITeamController { + $con = new Connection(get_database()); + return new \IQBall\Api\Controller\APITeamController(new \IQBall\Core\Model\TeamModel(new \IQBall\Core\Gateway\TeamGateway($con), new \IQBall\Core\Gateway\MemberGateway($con), new AccountGateway($con))); +} + + + function getRoutes(): AltoRouter { global $basePath; $router = new AltoRouter(); @@ -48,7 +56,6 @@ function getRoutes(): AltoRouter { $router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize())); $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); $router->map("POST", "/tactic/[i:id]/save", Action::auth(fn(int $id, Account $acc) => getTacticController()->saveContent($id, $acc))); - $router->map("GET", "/admin/list-users", Action::noAuth(fn() => getAccountController()->listUsers($_GET))); $router->map("GET", "/admin/user/[i:id]", Action::noAuth(fn(int $id) => getAccountController()->getUser($id))); $router->map("GET", "/admin/user/[i:id]/space", Action::noAuth(fn(int $id) => getTacticController()->getUserTactics($id))); @@ -56,6 +63,12 @@ function getRoutes(): AltoRouter { $router->map("POST", "/admin/user/remove-all", Action::noAuth(fn() => getAccountController()->removeUsers())); $router->map("POST", "/admin/user/[i:id]/update", Action::noAuth(fn(int $id) => getAccountController()->updateUser($id))); $router->map("GET", "/admin/server-info", Action::noAuth(fn() => getServerController()->getServerInfo())); + $router->map("GET", "/admin/list-team", Action::noAuth(fn() => getAPITeamController()->listTeams($_GET))); + $router->map("POST", "/admin/add-team", Action::noAuth(fn() => getAPITeamController()->addTeam())); + $router->map("POST", "/admin/delete-teams", Action::noAuth(fn() => getAPITeamController()->deleteTeamSelected())); + $router->map("POST", "/admin/team/[i:id]/update", Action::noAuth(fn(int $id) => getAPITeamController()->updateTeam($id))); + + return $router; } diff --git a/sql/database.php b/sql/database.php index 69b53e7..6d20c56 100644 --- a/sql/database.php +++ b/sql/database.php @@ -1,5 +1,9 @@ getHeaders() as $header => $value) { header("$header: $value"); } diff --git a/src/Api/Controller/APIAccountsController.php b/src/Api/Controller/APIAccountsController.php index 32fd956..6ebf0fc 100644 --- a/src/Api/Controller/APIAccountsController.php +++ b/src/Api/Controller/APIAccountsController.php @@ -3,7 +3,6 @@ namespace IQBall\Api\Controller; use IQBall\Api\APIControl; -use IQBall\App\Control; use IQBall\Core\Data\Account; use IQBall\Core\Gateway\AccountGateway; use IQBall\Core\Http\HttpCodes; @@ -25,6 +24,7 @@ class APIAccountsController { public function __construct(AuthModel $model, AccountGateway $accounts) { $this->accounts = $accounts; $this->authModel = $model; + } @@ -34,6 +34,7 @@ class APIAccountsController { */ public function listUsers(array $request): HttpResponse { return APIControl::runCheckedFrom($request, [ + 'start' => [DefaultValidators::isUnsignedInteger()], 'n' => [DefaultValidators::isIntInRange(0, 250)], 'search' => [DefaultValidators::lenBetween(0, 256)], diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 3cfe02e..eb00ff5 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -77,6 +77,7 @@ class APITacticController { 'name' => $t->getName(), 'court' => $t->getCourtType(), 'creation_date' => $t->getCreationDate(), + ], $tactics); return new JsonHttpResponse($response); diff --git a/src/Api/Controller/APITeamController.php b/src/Api/Controller/APITeamController.php new file mode 100644 index 0000000..270468d --- /dev/null +++ b/src/Api/Controller/APITeamController.php @@ -0,0 +1,79 @@ +teamModel = $teamModel; + } + + /** + * @param array $req_params + * @return HttpResponse + */ + public function listTeams(array $req_params): HttpResponse { + return APIControl::runCheckedFrom($req_params, [ + 'start' => [DefaultValidators::isUnsignedInteger()], + 'n' => [DefaultValidators::isUnsignedInteger()], + ], function (HttpRequest $req) { + $teams = $this->teamModel->listAll(intval($req['start']), intval($req['n'])); + return new JsonHttpResponse([ + "totalCount" => $this->teamModel->countTeam(), + "teams" => $teams, + ]); + }); + } + + public function addTeam(): HttpResponse { + return APIControl::runChecked([ + 'name' => [DefaultValidators::name()], + 'picture' => [DefaultValidators::isURL()], + 'mainColor' => [DefaultValidators::hexColor()], + 'secondaryColor' => [DefaultValidators::hexColor()], + + ], function (HttpRequest $req) { + $this->teamModel->createTeam($req['name'], $req['picture'], $req['mainColor'], $req['secondaryColor']); + return HttpResponse::fromCode(HttpCodes::OK); + }); + } + + public function deleteTeamSelected(): HttpResponse { + return APIControl::runChecked([ + 'teams' => [], + ], function (HttpRequest $req) { + $this->teamModel->deleteTeamSelected($req['teams']); + return HttpResponse::fromCode(HttpCodes::OK); + }); + } + + public function updateTeam(int $id): HttpResponse { + return APIControl::runChecked([ + 'name' => [DefaultValidators::name()], + 'picture' => [DefaultValidators::isURL()], + 'mainColor' => [DefaultValidators::hexColor()], + 'secondaryColor' => [DefaultValidators::hexColor()], + ], function (HttpRequest $req) { + $this->teamModel->editTeam($req['id'], $req['name'], $req['picture'], $req['mainColor'], $req['secondaryColor']); + return HttpResponse::fromCode(HttpCodes::OK); + }); + } + + +} diff --git a/src/App/App.php b/src/App/App.php index 1cfe6d7..557ad13 100644 --- a/src/App/App.php +++ b/src/App/App.php @@ -12,7 +12,6 @@ use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; -use Twig\Loader\FilesystemLoader; class App { /** @@ -90,6 +89,7 @@ class App { if ($action->getAuthType() == Action::AUTH_ADMIN && !$account->getUser()->isAdmin()) { return new JsonHttpResponse([ValidationFail::unauthorized()], HttpCodes::UNAUTHORIZED); } + } return $action->run($params, $session); diff --git a/src/Core/Control.php b/src/Core/Control.php index 106052d..51d6622 100644 --- a/src/Core/Control.php +++ b/src/Core/Control.php @@ -24,6 +24,7 @@ class Control { if (!$payload_obj instanceof \stdClass) { $fail = new ValidationFail("bad-payload", "request body is not a valid json object"); return $errorFactory->apply([$fail]); + } $payload = get_object_vars($payload_obj); return self::runCheckedFrom($payload, $schema, $run, $errorFactory); @@ -44,6 +45,7 @@ class Control { if (!empty($fails)) { return $errorFactory->apply($fails); + } return call_user_func_array($run, [$request]); diff --git a/src/Core/Data/User.php b/src/Core/Data/User.php index 02a44c0..8471929 100644 --- a/src/Core/Data/User.php +++ b/src/Core/Data/User.php @@ -24,7 +24,7 @@ class User implements \JsonSerializable { private string $profilePicture; /** - * @var bool isAdmin + * @var bool true if the user is an administrator */ private bool $isAdmin; diff --git a/src/Core/Gateway/AccountGateway.php b/src/Core/Gateway/AccountGateway.php index 6752b01..1a0c689 100644 --- a/src/Core/Gateway/AccountGateway.php +++ b/src/Core/Gateway/AccountGateway.php @@ -47,7 +47,6 @@ class AccountGateway { return !empty($result); } - /** * promote or demote a user to server administrator * @param int $id @@ -60,6 +59,7 @@ class AccountGateway { } else { $stmnt = $this->con->prepare("DELETE FROM Admins WHERE id = :id"); } + $stmnt->bindValue(':id', $id); $stmnt->execute(); @@ -155,6 +155,7 @@ class AccountGateway { ] ); return array_map(fn(array $acc) => new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profile_picture"], $this->isAdmin($acc["id"]))), $res); + } /** diff --git a/src/Core/Gateway/MemberGateway.php b/src/Core/Gateway/MemberGateway.php index 98d2d41..f79ff60 100644 --- a/src/Core/Gateway/MemberGateway.php +++ b/src/Core/Gateway/MemberGateway.php @@ -47,6 +47,7 @@ class MemberGateway { ] ); return array_map(fn($row) => new Member(new User($row['email'], $row['username'], $row['id'], $row['profile_picture'], $row['is_admin']), $teamId, $row['role']), $rows); + } /** diff --git a/src/Core/Gateway/TeamGateway.php b/src/Core/Gateway/TeamGateway.php index a817687..4309a49 100644 --- a/src/Core/Gateway/TeamGateway.php +++ b/src/Core/Gateway/TeamGateway.php @@ -3,6 +3,8 @@ namespace IQBall\Core\Gateway; use IQBall\Core\Connection; +use IQBall\Core\Data\Color; +use IQBall\Core\Data\Team; use IQBall\Core\Data\TeamInfo; use PDO; @@ -46,6 +48,7 @@ class TeamGateway { "id" => [$id, PDO::PARAM_INT], ] ); + return array_map(fn($row) => new TeamInfo($row['id'], $row['name'], $row['picture'], $row['main_color'], $row['second_color']), $result); } @@ -137,5 +140,47 @@ class TeamGateway { ); } + /** + * @param int $start + * @param int $n + * @return TeamInfo[] + */ + public function listAll(int $start, int $n): array { + $rows = $this->con->fetch( + "SELECT * FROM Team LIMIT :start, :n", + [ + ":start" => [$start, PDO::PARAM_INT], + ":n" => [$n, PDO::PARAM_INT], + ] + ); + return array_map(fn($row) => new TeamInfo($row['id'], $row['name'], $row['picture'], $row['main_color'], $row['second_color']), $rows); + } + + public function countTeam(): int { + $result = $this->con->fetch( + "SELECT count(*) as count FROM Team", + [] + ); + if (empty($result) || !isset($result[0]['count'])) { + return 0; + } + return $result[0]['count']; + } + + /** + * @param array $selectedTeams + * @return void + */ + public function deleteTeamSelected(array $selectedTeams): void { + foreach ($selectedTeams as $team) { + $this->con->exec( + "DELETE FROM TEAM WHERE id=:team", + [ + "team" => [$team, PDO::PARAM_INT], + ] + ); + } + } + } diff --git a/src/Core/Model/TeamModel.php b/src/Core/Model/TeamModel.php index 2bfe36e..b6b7bdd 100644 --- a/src/Core/Model/TeamModel.php +++ b/src/Core/Model/TeamModel.php @@ -45,10 +45,10 @@ class TeamModel { */ public function addMember(string $mail, int $teamId, string $role): int { $user = $this->users->getAccountFromMail($mail); - if($user == null) { + if ($user == null) { return -1; } - if(!$this->members->isMemberOfTeam($teamId, $user->getUser()->getId())) { + if (!$this->members->isMemberOfTeam($teamId, $user->getUser()->getId())) { $this->members->insert($teamId, $user->getUser()->getId(), $role); return 1; } @@ -70,7 +70,7 @@ class TeamModel { * @return Team|null */ public function getTeam(int $idTeam, int $idCurrentUser): ?Team { - if(!$this->members->isMemberOfTeam($idTeam, $idCurrentUser)) { + if (!$this->members->isMemberOfTeam($idTeam, $idCurrentUser)) { return null; } $teamInfo = $this->teams->getTeamById($idTeam); @@ -86,7 +86,7 @@ class TeamModel { */ public function deleteMember(int $idMember, int $teamId): int { $this->members->remove($teamId, $idMember); - if(empty($this->members->getMembersOfTeam($teamId))) { + if (empty($this->members->getMembersOfTeam($teamId))) { $this->teams->deleteTeam($teamId); return -1; } @@ -100,7 +100,7 @@ class TeamModel { * @return int */ public function deleteTeam(string $email, int $idTeam): int { - if($this->members->isCoach($email, $idTeam)) { + if ($this->members->isCoach($email, $idTeam)) { $this->teams->deleteTeam($idTeam); return 0; } @@ -139,4 +139,26 @@ class TeamModel { public function getAll(int $user): array { return $this->teams->getAll($user); } + + /** + * @param int $start + * @param int $n + * @return TeamInfo[] + */ + public function listAll(int $start, int $n) { + return $this->teams->listAll($start, $n); + } + + public function countTeam(): int { + return $this->teams->countTeam(); + } + + /** + * @param array $selectedTeams + * @return void + */ + public function deleteTeamSelected(array $selectedTeams) { + $this->teams->deleteTeamSelected($selectedTeams); + } + }