fix conflicts
continuous-integration/drone/push Build is failing Details

shareTactic
Vivien DUFOUR 1 year ago
commit 2ace144c6a

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

4
.gitignore vendored

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

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

@ -0,0 +1,81 @@
# 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 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.
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" 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.
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 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.
## 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". 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)

@ -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)
* [Description.md](Description.md)
* [Conception.md](Conception.md)
* [how-to-dev.md](how-to-dev.md)

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

@ -4,9 +4,9 @@ object Account {
<u>id
name
email
token
passwordHash
profilePicture
phone_number
password_hash
profile_picture
}
object TacticFolder {

@ -15,37 +15,51 @@ class HttpRequest implements ArrayAccess {
class HttpResponse {
- code: int
+ __construct(code: int)
- headers : array
+ __construct(code: int,headers:array)
+ getCode(): int
<u>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 {
+ <u>TWIG_VIEW: int {frozen}
+ <u>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
+ <u>twig(file: string, arguments: array, code: int = HttpCodes::OK): ViewHttpResponse
+ <u>react(file: string, arguments: array, code: int = HttpCodes::OK): ViewHttpResponse
+ <u>twig(file: string, arguments: array, code: int): ViewHttpResponse
+ <u>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

@ -6,70 +6,93 @@ 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 : MemberRole)
+ getUserId(): int
+ __construct(role : string)
+ getUser(): User
+ getTeamId(): int
+ getRole(): MemberRole
+ getRole(): string
}
Member --> "- role" MemberRole
enum MemberRole {
PLAYER
COACH
}
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(): Color
+ getSecondColor(): Color
+ getMainColor(): string
+ getSecondColor(): string
}
TeamInfo --> "- mainColor" Color
TeamInfo --> "- secondaryColor" Color
note left: Both team's color are the hex code of the color
class Team {
getInfo(): TeamInfo
listMembers(): Member[]
+ __construct(info:TeamInfo,members: Member[])
+ 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

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

@ -0,0 +1,44 @@
@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
class TacticValidator{
+ validateAccess(tacticId:int, tactic:?TacticInfo, ownerId:int): ?ValidationFail {static}
}
EditorController ..> TacticValidator
@enduml

@ -1,63 +1,87 @@
@startuml
class Team {
- name: string
- picture: Url
- members: array<int, MemberRole>
+ __construct(name : string, picture : string, mainColor : Colo, secondColor : Color)
+ getName(): string
+ getPicture(): Url
+ getMainColor(): Color
+ getSecondColor(): Color
+ listMembers(): array<Member>
}
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

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

@ -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()
+ <u>notFound(message: string): ValidationFail
+ <u>unauthorized(message:string): ValidationFail
+ <u>error(message:string): ValidationFail
}
class FieldValidationFail extends ValidationFail {
@ -49,13 +51,31 @@ class Validation {
<u> + validate(val: mixed, valName: string, failures: &array, validators: Validator...): bool
}
class Validators {
---
Validation ..> Validator
class DefaultValidators {
+ <u>nonEmpty(): Validator
+ <u>shorterThan(limit: int): Validator
+ <u>userString(maxLen: int): Validator
...
+ <u>regex(regex:string, msg:string): Validator
+ <u>hex(msg:string): Validator
+ <u>name(msg:string): Validator
+ <u>nameWithSpaces(): Validator
+ <u>lenBetween(min:int, max:int): Validator
+ <u>email(msg:string): Validator
+ <u>isInteger(): Validator
+ <u>isIntInRange(min:int, max:int): Validator
+ <u>isURL(): Validator
}
DefaultValidators ..> Validator
class FunctionValidator{
- validate_fn: callable
+ __construct(validate_fn:callable)
+ validate(name:string, val:mixed): array
}
Validator <|-- FunctionValidator
@enduml

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

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

@ -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 '<?php\nconst ASSETS = [' > views-mappings.php
echo '<?php const ASSETS = [' > 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/

@ -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;
@ -17,7 +18,16 @@ 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;
const DATABASE_PASSWORD = _DATABASE_PASSWORD;
function init_database(PDO $pdo): void {
_init_database($pdo);
}

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

@ -15,7 +15,7 @@ export function Header({ user }: { user: User }) {
id="iqball"
className="clickable"
onClick={() => {
location.pathname = "/"
location.pathname = BASE + "/"
}}>
<span id="IQ">IQ</span>
<span id="Ball">Ball</span>
@ -26,6 +26,7 @@ export function Header({ user }: { user: User }) {
<img
id="img-account"
src={user.profilePicture}
onClick={() => {
location.pathname = BASE + "/settings"
}}

@ -25,16 +25,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"
}
}

@ -1,5 +1,9 @@
<?php
use IQBall\Core\Connection;
use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Model\AuthModel;
$hostname = getHostName();
$front_url = "http://$hostname:5173";
@ -14,3 +18,26 @@ function _asset(string $assetURI): string {
global $front_url;
return $front_url . "/" . $assetURI;
}
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 {
return "";
}

@ -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";
@ -20,3 +23,10 @@ function _asset(string $assetURI): string {
// fallback to the uri itself.
return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI);
}
function _init_database(PDO $pdo): void {}
function _get_base_path(): string {
return BASE_PATH;
}

@ -0,0 +1,4 @@
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.*$ ./index.php [NC,L,QSA]

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

Before

Width:  |  Height:  |  Size: 747 B

@ -0,0 +1,4 @@
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.*$ ./index.php [NC,L,QSA]

@ -3,10 +3,11 @@
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;
use IQBall\Api\Controller\APIAuthController;
use IQBall\Api\Controller\APIServerController;
use IQBall\Api\Controller\APITacticController;
use IQBall\App\Session\PhpSessionHandle;
use IQBall\Core\Action;
@ -20,6 +21,9 @@ use IQBall\Core\Gateway\TeamGateway;
use IQBall\Core\Model\TeamModel;
use IQBall\Core\Gateway\MemberGateway;
$basePath = get_base_path() . "/api";
function getTacticController(): APITacticController {
return new APITacticController(
new TacticModel(new TacticInfoGateway(new Connection(get_database())), new AccountGateway(new Connection(get_database()))),
@ -31,13 +35,46 @@ function getAuthController(): APIAuthController {
return new APIAuthController(new AuthModel(new AccountGateway(new Connection(get_database()))));
}
function getAccountController(): APIAccountsController {
$con = new Connection(get_database());
$gw = new AccountGateway($con);
return new APIAccountsController(new AuthModel($gw), $gw);
}
function getServerController(): APIServerController {
global $basePath;
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();
$router->setBasePath(get_public_path(__DIR__));
$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::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()));
$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)));
$router->map("POST", "/tactic/[i:id]/can-share", Action::auth(fn(int $id, Account $acc) => getTacticController()->canShareTactic($id, $acc)));
$router->map("POST", "/tactic/[i:id]/can-share-to-team", Action::auth(fn(int $id, Account $acc) => getTacticController()->canShareTacticToTeam($id, $acc)));
@ -70,4 +107,4 @@ function tryGetAuthorization(): ?Account {
return $gateway->getAccountFromToken($token);
}
Api::render(API::handleMatch(getRoutes()->match(), fn() => tryGetAuthorization()));
Api::consume(API::handleMatch(getRoutes()->match(), fn() => tryGetAuthorization()));

@ -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;
@ -142,6 +141,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());

@ -1,5 +1,9 @@
<?php
use IQBall\Core\Connection;
use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Model\AuthModel;
/**
* @return PDO The PDO instance of the configuration's database connexion.
*/
@ -22,6 +26,7 @@ function get_database(): PDO {
}
}
init_database($pdo);
return $pdo;
}

@ -7,6 +7,11 @@ DROP TABLE IF EXISTS Member;
DROP TABLE IF EXISTS TacticSharedTeam;
DROP TABLE IF EXISTS TacticSharedAccount;
CREATE TABLE Admins
(
id integer PRIMARY KEY REFERENCES Account
);
CREATE TABLE Account
(
id integer PRIMARY KEY AUTOINCREMENT,
@ -14,7 +19,7 @@ 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
);
CREATE TABLE Tactic
@ -28,13 +33,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,
@ -44,11 +42,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,
PRIMARY KEY(id_team, id_user),
FOREIGN KEY (id_team) REFERENCES Team (id),

@ -4,14 +4,20 @@ 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;
class API {
public static function render(HttpResponse $response): void {
public static function consume(HttpResponse $response): void {
http_response_code($response->getCode());
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: *');
foreach ($response->getHeaders() as $header => $value) {
header("$header: $value");
}
@ -26,14 +32,14 @@ class API {
/**
* @param array<string, mixed> $match
* @param callable $tryGetAuthorization function to return an authorisation object for the given action (if required)
* @param array<string, mixed>|false $match
* @param callable(): Account $tryGetAuthorization function to return account authorisation 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")]);
return new JsonHttpResponse([ValidationFail::notFound("not found")], HttpCodes::NOT_FOUND);
}
$action = $match['target'];
@ -41,15 +47,19 @@ class API {
throw new Exception("routed action is not an AppAction object.");
}
$auth = null;
$account = null;
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->isAuthRequired()) {
$auth = call_user_func($tryGetAuthorization);
if ($auth == null) {
return new JsonHttpResponse([ValidationFail::unauthorized("Missing or invalid 'Authorization' header.")]);
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);
}
}

@ -0,0 +1,45 @@
<?php
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;
use IQBall\Core\Validation\Validator;
class APIControl {
private static function errorFactory(): ControlSchemaErrorResponseFactory {
return new class () implements ControlSchemaErrorResponseFactory {
public function apply(array $failures): HttpResponse {
return new JsonHttpResponse($failures, HttpCodes::BAD_REQUEST);
}
};
}
/**
* Runs given callback, if the request's payload json validates the given schema.
* @param array<string, Validator[]> $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<string, mixed> $data the request's data array.
* @param array<string, Validator[]> $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());
}
}

@ -0,0 +1,109 @@
<?php
namespace IQBall\Api\Controller;
use IQBall\Api\APIControl;
use IQBall\Core\Data\Account;
use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Model\AuthModel;
use IQBall\Core\Validation\DefaultValidators;
use IQBall\Core\Validation\ValidationFail;
class APIAccountsController {
private AccountGateway $accounts;
private AuthModel $authModel;
/**
* @param AuthModel $model
* @param AccountGateway $accounts
*/
public function __construct(AuthModel $model, AccountGateway $accounts) {
$this->accounts = $accounts;
$this->authModel = $model;
}
/**
* @param array<string, mixed> $request
* @return HttpResponse
*/
public function listUsers(array $request): HttpResponse {
return APIControl::runCheckedFrom($request, [
'start' => [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"]);
$users = array_map(fn(Account $acc) => $acc->getUser(), $accounts);
return new JsonHttpResponse([
"users" => $users,
"totalCount" => $this->accounts->totalCount(),
]);
});
}
/**
* @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());
}
public function addUser(): HttpResponse {
return APIControl::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(),
]);
});
}
public function removeUsers(): HttpResponse {
return APIControl::runChecked([
"identifiers" => [DefaultValidators::array(), DefaultValidators::forall(DefaultValidators::isUnsignedInteger())],
], function (HttpRequest $req) {
$this->accounts->removeAccounts($req["identifiers"]);
return HttpResponse::fromCode(HttpCodes::OK);
});
}
public function updateUser(int $id): HttpResponse {
return APIControl::runChecked([
"email" => [DefaultValidators::email()],
"username" => [DefaultValidators::name()],
"isAdmin" => [DefaultValidators::bool()],
], function (HttpRequest $req) use ($id) {
$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);
}
$this->authModel->update($id, $req["email"], $req["username"], $req["isAdmin"]);
return HttpResponse::fromCode(HttpCodes::OK);
});
}
}

@ -2,13 +2,14 @@
namespace IQBall\Api\Controller;
use IQBall\Api\APIControl;
use IQBall\App\Control;
use IQBall\Core\Http\HttpCodes;
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;
@ -26,9 +27,9 @@ class APIAuthController {
* @return HttpResponse
*/
public function authorize(): HttpResponse {
return Control::runChecked([
"email" => [Validators::email(), Validators::lenBetween(5, 256)],
"password" => [Validators::lenBetween(6, 256)],
return APIControl::runChecked([
"email" => [DefaultValidators::email(), DefaultValidators::lenBetween(5, 256)],
"password" => [DefaultValidators::password()],
], function (HttpRequest $req) {
$failures = [];
$account = $this->model->login($req["email"], $req["password"], $failures);
@ -40,5 +41,4 @@ class APIAuthController {
return new JsonHttpResponse(["authorization" => $account->getToken()]);
});
}
}

@ -0,0 +1,45 @@
<?php
namespace IQBall\Api\Controller;
use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse;
class APIServerController {
private string $basePath;
private \PDO $pdo;
/**
* @param string $basePath
* @param \PDO $pdo
*/
public function __construct(string $basePath, \PDO $pdo) {
$this->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)",
],
]);
}
}

@ -2,8 +2,10 @@
namespace IQBall\Api\Controller;
use IQBall\App\Control;
use IQBall\Api\APIControl;
use IQBall\Core\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;
@ -13,6 +15,7 @@ use IQBall\Core\Model\TeamModel;
use IQBall\Core\Validation\FieldValidationFail;
use IQBall\Core\Validation\Validators;
/**
* API endpoint related to tactics
*/
@ -36,8 +39,8 @@ class APITacticController {
* @return HttpResponse
*/
public function updateName(int $tactic_id, Account $account): HttpResponse {
return Control::runChecked([
"name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()],
return APIControl::runChecked([
"name" => [DefaultValidators::lenBetween(1, 50), DefaultValidators::nameWithSpaces()],
], function (HttpRequest $request) use ($tactic_id, $account) {
$failures = $this->tacticModel->updateName($tactic_id, $request["name"], $account->getUser()->getId());
@ -57,7 +60,7 @@ class APITacticController {
* @return HttpResponse
*/
public function saveContent(int $id, Account $account): HttpResponse {
return Control::runChecked([
return APIControl::runChecked([
"content" => [],
], function (HttpRequest $req) use ($id) {
if ($fail = $this->tacticModel->updateContent($id, json_encode($req["content"]))) {
@ -154,4 +157,23 @@ class APITacticController {
return new JsonHttpResponse(["message" => "Action non autorisée"], HttpCodes::FORBIDDEN);
});
}
/**
* @param int $userId
* @return HttpResponse given user information.
*/
public function getUserTactics(int $userId): HttpResponse {
$tactics = $this->tacticModel->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);
}
}

@ -0,0 +1,79 @@
<?php
namespace IQBall\Api\Controller;
use IQBall\Api\APIControl;
use IQBall\Core\Data\Account;
use IQBall\Core\Data\Team;
use IQBall\Core\Data\TeamInfo;
use IQBall\Core\Gateway\TeamGateway;
use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Model\TeamModel;
use IQBall\Core\Validation\DefaultValidators;
class APITeamController {
private TeamModel $teamModel;
/**
* @param TeamModel $teamModel
*/
public function __construct(TeamModel $teamModel) {
$this->teamModel = $teamModel;
}
/**
* @param array<string, mixed> $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);
});
}
}

@ -4,13 +4,14 @@ 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;
use Twig\Error\SyntaxError;
use Twig\Loader\FilesystemLoader;
class App {
/**
@ -77,13 +78,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);
return HttpResponse::redirectAbsolute($authRoute);
}
if ($action->getAuthType() == Action::AUTH_ADMIN && !$account->getUser()->isAdmin()) {
return new JsonHttpResponse([ValidationFail::unauthorized()], HttpCodes::UNAUTHORIZED);
}
}
return $action->run($params, $session);

@ -0,0 +1,44 @@
<?php
namespace IQBall\App;
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\Validation\Validator;
class AppControl {
private static function errorFactory(): ControlSchemaErrorResponseFactory {
return new class () implements ControlSchemaErrorResponseFactory {
public function apply(array $failures): HttpResponse {
return ViewHttpResponse::twig("error.html.twig", ['failures' => $failures], HttpCodes::BAD_REQUEST);
}
};
}
/**
* Runs given callback, if the request's payload json validates the given schema.
* @param array<string, Validator[]> $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<string, mixed> $data the request's data array.
* @param array<string, Validator[]> $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());
}
}

@ -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\Validators;
use IQBall\Core\Validation\DefaultValidators;
use IQBall\Core\Validation\FieldValidationFail;
class AuthController {
private AuthModel $model;
@ -32,24 +34,36 @@ 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::password()],
"confirmpassword" => [DefaultValidators::password()],
"email" => [DefaultValidators::email(), DefaultValidators::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']]);
}
}
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['confirmpassword'], $request['email'], $fails);
$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]);
}
$session->setAccount($account);
$target_url = $session->getInitialTarget();
if ($target_url != null) {
return HttpResponse::redirect_absolute($target_url);
return HttpResponse::redirectAbsolute($target_url);
}
return HttpResponse::redirect("/home");
@ -78,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");

@ -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) {
@ -169,7 +169,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]);
@ -229,10 +229,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 = [];

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

@ -62,6 +62,10 @@
text-align: right;
}
.consentement{
font-size: small;
}
#buttons{
display: flex;
justify-content: center;
@ -95,15 +99,18 @@
{% endfor %}
<label for="username">Nom d'utilisateur :</label>
<input type="text" id="username" name="username" required>
<input type="text" id="username" name="username" value="{{ username }}"required>
<label for="password">Mot de passe :</label>
<input type="password" id="password" name="password" required>
<label for="confirmpassword">Confirmer le mot de passe :</label>
<input type="password" id="confirmpassword" name="confirmpassword" required>
<label for="email">Email :</label>
<input type="text" id="email" name="email" required>
<input type="text" id="email" name="email" value="{{ email }}"required><br><br>
<label class="consentement">
<input type="checkbox" name="consentement" required>
En cochant cette case, j'accepte que mes données personnelles, tel que mon adresse e-mail, soient collectées et traitées conformément à la politique de confidentialité de Sportify.
</label><br>
<a href="{{ path('/login') }}" class="inscr">Vous avez déjà un compte ?</a>
</div>
<div id = "buttons">
<input class = "button" type="submit" value="Créer votre compte">

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

@ -1,46 +1,51 @@
<?php
namespace IQBall\App;
namespace IQBall\Core;
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;
class Control {
/**
* Runs given callback, if the request's json validates the given schema.
* @param array<string, Validator[]> $schema an array of `fieldName => Validators` which represents the request object schema
* Runs given callback, if the request's payload json validates the given schema.
* @param array<string, Validator[]> $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 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): 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");
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);
return self::runCheckedFrom($payload, $schema, $run, $errorFactory);
}
/**
* Runs given callback, if the given request data array validates the given schema.
* @param array<string, mixed> $data the request's data array.
* @param array<string, Validator[]> $schema an array of `fieldName => Validators` which represents the request object schema
* @param array<string, Validator[]> $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 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): HttpResponse {
public static function runCheckedFrom(array $data, array $schema, callable $run, ControlSchemaErrorResponseFactory $errorFactory): HttpResponse {
$fails = [];
$request = HttpRequest::from($data, $fails, $schema);
if (!empty($fails)) {
return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST);
return $errorFactory->apply($fails);
}
return call_user_func_array($run, [$request]);

@ -0,0 +1,14 @@
<?php
namespace IQBall\Core;
use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Validation\ValidationFail;
interface ControlSchemaErrorResponseFactory {
/**
* @param ValidationFail[] $failures
* @return HttpResponse
*/
public function apply(array $failures): HttpResponse;
}

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

@ -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 true if the user is an administrator
*/
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;
}
/**

@ -26,16 +26,54 @@ class AccountGateway {
* @return int
*/
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,: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 WHERE id = :id", [
':username' => [$name, PDO::PARAM_STR],
':email' => [$email, PDO::PARAM_STR],
':token' => [$token, PDO::PARAM_STR],
':id' => [$id, PDO::PARAM_INT],
]);
$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);
}
/**
* 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 {
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();
return $stmnt->rowCount() > 0;
}
/**
* @param string $email
* @return array<string, mixed>|null
@ -74,7 +112,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"], $this->isAdmin($acc["id"])));
}
/**
@ -95,12 +133,50 @@ 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"], $this->isAdmin($acc["id"])));
}
/**
* 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 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",
[
":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"], $this->isAdmin($acc["id"]))), $res);
}
/**
@ -149,5 +225,23 @@ class AccountGateway {
return intval($this->con->lastInsertId());
}
/**
* 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],
]);
}
}
}

@ -41,12 +41,13 @@ 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);
}
/**

@ -197,6 +197,24 @@ class TacticInfoGateway {
);
}
/**
* 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

@ -94,6 +94,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);
}
@ -211,4 +212,46 @@ 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<Team> $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],
]
);
}
}
}

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

@ -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,40 +25,34 @@ 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): ?Account {
if ($password != $confirmPassword) {
$failures[] = new FieldValidationFail("confirmpassword", "Le mot de passe et la confirmation ne sont pas les mêmes.");
}
public function register(
string $username,
string $password,
string $email
): ?Account {
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));
return new Account($token, new User($email, $username, $accountId, self::DEFAULT_PROFILE_PICTURE, false));
}
/**
* Generate a random base 64 string
* @return string
* @throws Exception
*/
private function generateToken(): string {
return base64_encode(random_bytes(64));
public static function generateToken(): string {
try {
return base64_encode(random_bytes(64));
} catch (Exception $e) {
throw new \RuntimeException($e);
}
}
/**
@ -74,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);
}
}

@ -66,6 +66,18 @@ class TacticModel {
return $this->tactics->getLastOwnedBy($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[]
*/
public function listAllOf(int $user_id): array {
return$this->tactics->listAllOf($user_id);
}
/**
* Get all the tactics of the owner
*

@ -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);
@ -128,7 +128,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;
}
@ -142,7 +142,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;
}
@ -199,4 +199,26 @@ class TeamModel {
public function getAllTeamTactic(int $team): array {
return $this->teams->getAllTeamTactic($team);
}
/**
* @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<Team> $selectedTeams
* @return void
*/
public function deleteTeamSelected(array $selectedTeams) {
$this->teams->deleteTeamSelected($selectedTeams);
}
}

@ -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
*/
@ -38,6 +38,10 @@ class Validators {
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
@ -68,7 +72,11 @@ class Validators {
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 {
@ -78,10 +86,61 @@ class Validators {
);
}
/**
* @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 = [];
foreach ($val as $idx => $item) {
$failures = array_merge($failures, $this->validator->validate($name . "[$idx]", $item));
}
return $failures;
}
};
}
}

@ -1,21 +0,0 @@
<?php
/**
* relative path of the public directory from the server's document root.
*/
function get_public_path(string $public_dir): string {
// find the server path of the index.php file
$basePath = substr($public_dir, strlen($_SERVER['DOCUMENT_ROOT']));
$basePathLen = strlen($basePath);
if ($basePathLen == 0) {
return "";
}
$c = $basePath[$basePathLen - 1];
if ($c == "/" || $c == "\\") {
$basePath = substr($basePath, 0, $basePathLen - 1);
}
return $basePath;
}
Loading…
Cancel
Save