diff --git a/.gitignore b/.gitignore index 9124809..1c4404c 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ package-lock.json npm-debug.log* yarn-debug.log* yarn-error.log* + +.php-cs-fixer.cache \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..77ef0e7 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,16 @@ +in(__DIR__); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PER-CS' => true, + '@PHP74Migration' => true, + 'array_syntax' => ['syntax' => 'short'], + 'braces_position' => [ + 'classes_opening_brace' => 'same_line', + 'functions_opening_brace' => 'same_line' + ] + ]) + ->setIndent(" ") + ->setFinder($finder); diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7db0434 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "bracketSameLine": true, + "trailingComma": "all", + "printWidth": 80, + "tabWidth": 4, + "semi": false +} \ No newline at end of file diff --git a/Documentation/conception.puml b/Documentation/conception.puml deleted file mode 100644 index 06ae256..0000000 --- a/Documentation/conception.puml +++ /dev/null @@ -1,12 +0,0 @@ -@startuml - -class Connexion - -class Modele - -class Account - -class AccountGateway - - -@enduml diff --git a/Documentation/models.puml b/Documentation/models.puml index 0ad5135..d95343c 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -50,7 +50,6 @@ enum MemberRole { class Team { - name: String - picture: Url - - members: array + getName(): String + getPicture(): Url @@ -61,6 +60,7 @@ class Team { Team --> "- mainColor" Color Team --> "- secondaryColor" Color +Team --> "- members *" Member class Color { - value: int @@ -68,4 +68,30 @@ class Color { + getValue(): int } +class AuthController{ + + + displayRegister() : HttpResponse + + displayBadFields(viewName : string, fails : array) : HttpResponse + + confirmRegister(request : array) : HttpResponse + + displayLogin() : HttpResponse + + confirmLogin() : HttpResponse +} +AuthController --> "- model" AuthModel + +class AuthModel{ + + + register(username : string, password : string, confirmPassword : string, email : string): array + + getUserFields(email : string):array + + login(email : string, password : string) +} +AuthModel --> "- gateway" AuthGateway + +class AuthGateway{ + -con : Connection + + + mailExist(email : string) : bool + + insertAccount(username : string, hash : string, email : string) + + getUserHash(email : string):string + + getUserFields (email : string): array +} @enduml \ No newline at end of file diff --git a/ci/.drone.yml b/ci/.drone.yml index d6c474c..8b7058d 100644 --- a/ci/.drone.yml +++ b/ci/.drone.yml @@ -1,6 +1,6 @@ kind: pipeline type: docker -name: "Deploy on maxou.dev" +name: "CI and Deploy on maxou.dev" volumes: - name: server @@ -11,11 +11,26 @@ trigger: - push steps: + + - image: node:latest + name: "front CI" + commands: + - npm install + - npm run tsc + + - image: composer:latest + name: "php CI" + commands: + - composer install + - vendor/bin/phpstan analyze + - image: node:latest name: "build node" volumes: &outputs - name: server path: /outputs + depends_on: + - "front CI" commands: - curl -L moshell.dev/setup.sh > /tmp/moshell_setup.sh - chmod +x /tmp/moshell_setup.sh @@ -24,14 +39,15 @@ steps: - - /root/.local/bin/moshell ci/build_react.msh - - image: composer:latest + - image: ubuntu:latest name: "prepare php" volumes: *outputs + depends_on: + - "php CI" 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 - - composer install && composer update - rm profiles/dev-config-profile.php - mv src config.php sql profiles vendor /outputs/ diff --git a/ci/build_react.msh b/ci/build_react.msh index 203afa0..64a0cb6 100755 --- a/ci/build_react.msh +++ b/ci/build_react.msh @@ -3,7 +3,12 @@ mkdir -p /outputs/public apt update && apt install jq -y -npm install + +val drone_branch = std::env("DRONE_BRANCH").unwrap() + +val base = "/IQBall/$drone_branch/public" +npm run build -- --base=$base --mode PROD + npm run build -- --base=/IQBall/public --mode PROD // Read generated mappings from build diff --git a/composer.json b/composer.json index 0f5e1f2..a3c0e4b 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,10 @@ "ext-json": "*", "ext-pdo": "*", "ext-pdo_sqlite": "*", - "twig/twig":"^2.0" + "twig/twig":"^2.0", + "phpstan/phpstan": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.38" } - -} \ No newline at end of file +} diff --git a/config.php b/config.php index 592ee38..6e510c8 100644 --- a/config.php +++ b/config.php @@ -5,7 +5,7 @@ // Please do not touch. require /*PROFILE_FILE*/ "profiles/dev-config-profile.php"; -CONST SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH; +const SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH; /** * Maps the given relative source uri (relative to the `/front` folder) to its actual location depending on imported profile. @@ -20,4 +20,3 @@ global $_data_source_name; $data_source_name = $_data_source_name; const DATABASE_USER = _DATABASE_USER; const DATABASE_PASSWORD = _DATABASE_PASSWORD; - diff --git a/format.sh b/format.sh new file mode 100755 index 0000000..a9ff1c2 --- /dev/null +++ b/format.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +## verify php and typescript types + +echo "formatting php typechecking" +vendor/bin/php-cs-fixer fix + +echo "formatting typescript typechecking" +npm run format diff --git a/front/Constants.ts b/front/Constants.ts index aaaaa43..76b37c2 100644 --- a/front/Constants.ts +++ b/front/Constants.ts @@ -1,4 +1,4 @@ /** * This constant defines the API endpoint. */ -export const API = import.meta.env.VITE_API_ENDPOINT; \ No newline at end of file +export const API = import.meta.env.VITE_API_ENDPOINT diff --git a/front/ViewRenderer.tsx b/front/ViewRenderer.tsx index 17148dc..57f2e34 100644 --- a/front/ViewRenderer.tsx +++ b/front/ViewRenderer.tsx @@ -1,5 +1,5 @@ -import ReactDOM from "react-dom/client"; -import React, {FunctionComponent} from "react"; +import ReactDOM from "react-dom/client" +import React, { FunctionComponent } from "react" /** * Dynamically renders a React component, with given arguments @@ -8,14 +8,12 @@ import React, {FunctionComponent} from "react"; */ export function renderView(Component: FunctionComponent, args: {}) { const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement - ); - - console.log(args) + document.getElementById("root") as HTMLElement, + ) root.render( - - - ); -} \ No newline at end of file + + , + ) +} diff --git a/front/assets/basketball_court.svg b/front/assets/basketball_court.svg new file mode 100644 index 0000000..e0df003 --- /dev/null +++ b/front/assets/basketball_court.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/front/assets/icon/remove.svg b/front/assets/icon/remove.svg new file mode 100644 index 0000000..6886097 --- /dev/null +++ b/front/assets/icon/remove.svg @@ -0,0 +1,5 @@ + + + + diff --git a/front/components/Rack.tsx b/front/components/Rack.tsx new file mode 100644 index 0000000..2a7511f --- /dev/null +++ b/front/components/Rack.tsx @@ -0,0 +1,72 @@ +import { ReactElement, useRef } from "react" +import Draggable from "react-draggable" + +export interface RackProps { + id: string + objects: E[] + onChange: (objects: E[]) => void + canDetach: (ref: HTMLDivElement) => boolean + onElementDetached: (ref: HTMLDivElement, el: E) => void + render: (e: E) => ReactElement +} + +interface RackItemProps { + item: E + onTryDetach: (ref: HTMLDivElement, el: E) => void + render: (e: E) => ReactElement +} + +/** + * A container of draggable objects + * */ +export function Rack({ + id, + objects, + onChange, + canDetach, + onElementDetached, + render, +}: RackProps) { + return ( +
+ {objects.map((element) => ( + { + if (!canDetach(ref)) return + + const index = objects.findIndex( + (o) => o.key === element.key, + ) + onChange(objects.toSpliced(index, 1)) + + onElementDetached(ref, element) + }} + /> + ))} +
+ ) +} + +function RackItem({ + item, + onTryDetach, + render, +}: RackItemProps) { + const divRef = useRef(null) + + return ( + onTryDetach(divRef.current!, item)}> +
{render(item)}
+
+ ) +} diff --git a/front/components/TitleInput.tsx b/front/components/TitleInput.tsx index eb162d1..6e4acb0 100644 --- a/front/components/TitleInput.tsx +++ b/front/components/TitleInput.tsx @@ -1,28 +1,32 @@ -import React, {CSSProperties, useRef, useState} from "react"; -import "../style/title_input.css"; +import React, { CSSProperties, useRef, useState } from "react" +import "../style/title_input.css" export interface TitleInputOptions { - style: CSSProperties, - default_value: string, + style: CSSProperties + default_value: string on_validated: (a: string) => void } -export default function TitleInput({style, default_value, on_validated}: TitleInputOptions) { - const [value, setValue] = useState(default_value); - const ref = useRef(null); +export default function TitleInput({ + style, + default_value, + on_validated, +}: TitleInputOptions) { + const [value, setValue] = useState(default_value) + const ref = useRef(null) return ( - setValue(event.target.value)} - onBlur={_ => on_validated(value)} - onKeyDown={event => { - if (event.key == 'Enter') - ref.current?.blur(); - }} + setValue(event.target.value)} + onBlur={(_) => on_validated(value)} + onKeyDown={(event) => { + if (event.key == "Enter") ref.current?.blur() + }} /> ) -} \ No newline at end of file +} diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx new file mode 100644 index 0000000..9f4cb5d --- /dev/null +++ b/front/components/editor/BasketCourt.tsx @@ -0,0 +1,27 @@ +import CourtSvg from "../../assets/basketball_court.svg?react" +import "../../style/basket_court.css" +import { useRef } from "react" +import CourtPlayer from "./CourtPlayer" +import { Player } from "../../data/Player" + +export interface BasketCourtProps { + players: Player[] + onPlayerRemove: (p: Player) => void +} + +export function BasketCourt({ players, onPlayerRemove }: BasketCourtProps) { + return ( +
+ + {players.map((player) => { + return ( + onPlayerRemove(player)} + /> + ) + })} +
+ ) +} diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx new file mode 100644 index 0000000..9b08e7b --- /dev/null +++ b/front/components/editor/CourtPlayer.tsx @@ -0,0 +1,49 @@ +import { useRef } from "react" +import "../../style/player.css" +import RemoveIcon from "../../assets/icon/remove.svg?react" +import Draggable from "react-draggable" +import { PlayerPiece } from "./PlayerPiece" +import { Player } from "../../data/Player" + +export interface PlayerProps { + player: Player + onRemove: () => void +} + +/** + * A player that is placed on the court, which can be selected, and moved in the associated bounds + * */ +export default function CourtPlayer({ player, onRemove }: PlayerProps) { + const ref = useRef(null) + + const x = player.rightRatio + const y = player.bottomRatio + + return ( + +
+
{ + if (e.key == "Delete") onRemove() + }}> +
+ +
+ +
+
+
+ ) +} diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx new file mode 100644 index 0000000..08bf36d --- /dev/null +++ b/front/components/editor/PlayerPiece.tsx @@ -0,0 +1,11 @@ +import React from "react" +import "../../style/player.css" +import { Team } from "../../data/Team" + +export function PlayerPiece({ team, text }: { team: Team; text: string }) { + return ( +
+

{text}

+
+ ) +} diff --git a/front/data/Player.ts b/front/data/Player.ts new file mode 100644 index 0000000..f2667b9 --- /dev/null +++ b/front/data/Player.ts @@ -0,0 +1,29 @@ +import { Team } from "./Team" + +export interface Player { + /** + * unique identifier of the player. + * This identifier must be unique to the associated court. + */ + id: number + + /** + * the player's team + * */ + team: Team + + /** + * player's position + * */ + role: string + + /** + * Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) + */ + bottomRatio: number + + /** + * Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle) + */ + rightRatio: number +} diff --git a/front/data/Team.tsx b/front/data/Team.tsx new file mode 100644 index 0000000..5b35943 --- /dev/null +++ b/front/data/Team.tsx @@ -0,0 +1,4 @@ +export enum Team { + Allies = "allies", + Opponents = "opponents", +} diff --git a/front/style/basket_court.css b/front/style/basket_court.css new file mode 100644 index 0000000..c001cc0 --- /dev/null +++ b/front/style/basket_court.css @@ -0,0 +1,15 @@ +#court-container { + display: flex; + + background-color: var(--main-color); +} + +#court-svg { + margin: 5%; + user-select: none; + -webkit-user-drag: none; +} + +#court-svg * { + stroke: var(--selected-team-secondarycolor); +} diff --git a/front/style/colors.css b/front/style/colors.css index 34bdbb5..3c17a25 100644 --- a/front/style/colors.css +++ b/front/style/colors.css @@ -1,8 +1,11 @@ - - :root { --main-color: #ffffff; --second-color: #ccde54; --background-color: #d2cdd3; -} \ No newline at end of file + + --selected-team-primarycolor: #ffffff; + --selected-team-secondarycolor: #000000; + + --selection-color: #3f7fc4; +} diff --git a/front/style/editor.css b/front/style/editor.css index 2ed88d6..b586a36 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -1,13 +1,15 @@ @import "colors.css"; - -#main { +#main-div { + display: flex; height: 100%; width: 100%; background-color: var(--background-color); + + flex-direction: column; } -#topbar { +#topbar-div { display: flex; background-color: var(--main-color); @@ -15,6 +17,42 @@ align-items: stretch; } +#racks { + display: flex; + justify-content: space-between; +} + .title_input { width: 25ch; -} \ No newline at end of file +} + +#edit-div { + height: 100%; +} + +#allies-rack .player-piece, +#opponent-rack .player-piece { + margin-left: 5px; +} + +.player-piece.opponents { + background-color: #f59264; +} + +#court-div { + background-color: var(--background-color); + height: 100%; + + display: flex; + align-items: center; + justify-content: center; + align-content: center; +} + +#court-div-bounds { + width: 60%; +} + +.react-draggable { + z-index: 2; +} diff --git a/front/style/player.css b/front/style/player.css new file mode 100644 index 0000000..7bea36e --- /dev/null +++ b/front/style/player.css @@ -0,0 +1,79 @@ +/** +as the .player div content is translated, +the real .player div position is not were the user can expect. +Disable pointer events to this div as it may overlap on other components +on the court. +*/ +.player { + pointer-events: none; +} + +.player-content { + display: flex; + flex-direction: column; + align-content: center; + align-items: center; + outline: none; +} + +.player-piece { + font-family: monospace; + pointer-events: all; + + background-color: var(--selected-team-primarycolor); + color: var(--selected-team-secondarycolor); + + border-width: 2px; + border-radius: 100px; + border-style: solid; + + width: 20px; + height: 20px; + + display: flex; + + align-items: center; + justify-content: center; + + user-select: none; +} + +.player-selection-tab { + display: flex; + + position: absolute; + margin-bottom: 10%; + justify-content: center; + visibility: hidden; + + width: 100%; + transform: translateY(-20px); +} + +.player-selection-tab-remove { + pointer-events: all; + height: 25%; +} + +.player-selection-tab-remove * { + stroke: red; + fill: white; +} + +.player-selection-tab-remove:hover * { + fill: #f1dbdb; + stroke: #ff331a; + cursor: pointer; +} + +.player:focus-within .player-selection-tab { + visibility: visible; +} + +.player:focus-within .player-piece { + color: var(--selection-color); +} + +.player:focus-within { + z-index: 1000; +} diff --git a/front/style/title_input.css b/front/style/title_input.css index 57af59b..1b6be10 100644 --- a/front/style/title_input.css +++ b/front/style/title_input.css @@ -14,4 +14,3 @@ border-bottom-color: blueviolet; } - diff --git a/front/style/visualizer.css b/front/style/visualizer.css new file mode 100644 index 0000000..2d1a73f --- /dev/null +++ b/front/style/visualizer.css @@ -0,0 +1,30 @@ +#main { + height: 100vh; + width: 100%; + display: flex; + flex-direction: column; +} + +#topbar { + display: flex; + background-color: var(--main-color); + justify-content: center; + align-items: center; +} + +h1 { + text-align: center; + margin-top: 0; +} + +#court-container { + flex: 1; + display: flex; + justify-content: center; + background-color: var(--main-color); +} + +#court { + max-width: 80%; + max-height: 80%; +} diff --git a/front/views/DisplayResults.tsx b/front/views/DisplayResults.tsx index c4bbd1b..7e22df3 100644 --- a/front/views/DisplayResults.tsx +++ b/front/views/DisplayResults.tsx @@ -1,19 +1,13 @@ - interface DisplayResultsProps { - results: readonly { name: string, description: string}[] + results: readonly { name: string; description: string }[] } -export default function DisplayResults({results}: DisplayResultsProps) { - const list = results - .map(({name, description}) => +export default function DisplayResults({ results }: DisplayResultsProps) { + const list = results.map(({ name, description }) => (

username: {name}

description: {description}

- ) - return ( -
- {list} -
- ) + )) + return
{list}
} diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 84d24e6..d98062d 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -1,41 +1,162 @@ -import React, {CSSProperties, useState} from "react"; -import "../style/editor.css"; -import TitleInput from "../components/TitleInput"; -import {API} from "../Constants"; +import { CSSProperties, useRef, useState } from "react" +import "../style/editor.css" +import TitleInput from "../components/TitleInput" +import { API } from "../Constants" +import { BasketCourt } from "../components/editor/BasketCourt" + +import { Rack } from "../components/Rack" +import { PlayerPiece } from "../components/editor/PlayerPiece" +import { Player } from "../data/Player" +import { Team } from "../data/Team" const ERROR_STYLE: CSSProperties = { - borderColor: "red" + borderColor: "red", +} + +/** + * information about a player that is into a rack + */ +interface RackedPlayer { + team: Team + key: string } -export default function Editor({id, name}: { id: number, name: string }) { +export default function Editor({ id, name }: { id: number; name: string }) { + const [style, setStyle] = useState({}) + const positions = ["1", "2", "3", "4", "5"] + const [allies, setAllies] = useState( + positions.map((key) => ({ team: Team.Allies, key })), + ) + const [opponents, setOpponents] = useState( + positions.map((key) => ({ team: Team.Opponents, key })), + ) + + const [players, setPlayers] = useState([]) + const courtDivContentRef = useRef(null) + + const canDetach = (ref: HTMLDivElement) => { + const refBounds = ref.getBoundingClientRect() + const courtBounds = courtDivContentRef.current!.getBoundingClientRect() - const [style, setStyle] = useState({}); + // check if refBounds overlaps courtBounds + return !( + refBounds.top > courtBounds.bottom || + refBounds.right < courtBounds.left || + refBounds.bottom < courtBounds.top || + refBounds.left > courtBounds.right + ) + } + + const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => { + const refBounds = ref.getBoundingClientRect() + const courtBounds = courtDivContentRef.current!.getBoundingClientRect() + + const relativeXPixels = refBounds.x - courtBounds.x + const relativeYPixels = refBounds.y - courtBounds.y + + const xRatio = relativeXPixels / courtBounds.width + const yRatio = relativeYPixels / courtBounds.height + + setPlayers((players) => { + return [ + ...players, + { + id: players.length, + team: element.team, + role: element.key, + rightRatio: xRatio, + bottomRatio: yRatio, + }, + ] + }) + } return ( -
-
+
+
LEFT
- { - fetch(`${API}/tactic/${id}/edit/name`, { - method: "POST", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - name: new_name, + { + fetch(`${API}/tactic/${id}/edit/name`, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: new_name, + }), + }).then((response) => { + if (response.ok) { + setStyle({}) + } else { + setStyle(ERROR_STYLE) + } }) - }).then(response => { - if (response.ok) { - setStyle({}) - } else { - setStyle(ERROR_STYLE) - } - }) - }}/> + }} + />
RIGHT
+
+
+ ( + + )} + /> + ( + + )} + /> +
+
+
+ { + setPlayers((players) => { + const idx = players.indexOf(player) + return players.toSpliced(idx, 1) + }) + switch (player.team) { + case Team.Opponents: + setOpponents((opponents) => [ + ...opponents, + { + team: player.team, + pos: player.role, + key: player.role, + }, + ]) + break + case Team.Allies: + setAllies((allies) => [ + ...allies, + { + team: player.team, + pos: player.role, + key: player.role, + }, + ]) + } + }} + /> +
+
+
) } - diff --git a/front/views/SampleForm.tsx b/front/views/SampleForm.tsx index 604e362..00309e4 100644 --- a/front/views/SampleForm.tsx +++ b/front/views/SampleForm.tsx @@ -1,19 +1,14 @@ - - export default function SampleForm() { return (

Hello, this is a sample form made in react !

- + - - + +
) } - - - diff --git a/front/views/Visualizer.tsx b/front/views/Visualizer.tsx new file mode 100644 index 0000000..541da09 --- /dev/null +++ b/front/views/Visualizer.tsx @@ -0,0 +1,23 @@ +import React, { CSSProperties, useState } from "react" +import "../style/visualizer.css" +import Court from "../assets/basketball_court.svg" + +export default function Visualizer({ id, name }: { id: number; name: string }) { + const [style, setStyle] = useState({}) + + return ( +
+
+

{name}

+
+
+ Basketball Court +
+
+ ) +} diff --git a/package.json b/package.json index 0eb1e79..79c9d46 100644 --- a/package.json +++ b/package.json @@ -12,15 +12,17 @@ "@types/react-dom": "^18.2.14", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", "typescript": "^5.2.2", "vite": "^4.5.0", - "vite-plugin-css-injected-by-js": "^3.3.0", - "web-vitals": "^2.1.4" + "vite-plugin-css-injected-by-js": "^3.3.0" }, "scripts": { "start": "vite --host", "build": "vite build", - "test": "vite test" + "test": "vite test", + "format": "prettier --config .prettierrc 'front' --write", + "tsc": "tsc" }, "eslintConfig": { "extends": [ @@ -29,6 +31,9 @@ ] }, "devDependencies": { - "@vitejs/plugin-react": "^4.1.0" + "@vitejs/plugin-react": "^4.1.0", + "vite-plugin-svgr": "^4.1.0", + "prettier": "^3.1.0", + "typescript": "^5.2.2" } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..bc6c041 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,13 @@ +parameters: + phpVersion: 70400 + level: 6 + paths: + - src + - public + scanFiles: + - config.php + - sql/database.php + - profiles/dev-config-profile.php + - profiles/prod-config-profile.php + excludePaths: + - src/react-display-file.php diff --git a/profiles/dev-config-profile.php b/profiles/dev-config-profile.php index 316ff44..bd87f1d 100644 --- a/profiles/dev-config-profile.php +++ b/profiles/dev-config-profile.php @@ -10,11 +10,7 @@ $_data_source_name = "sqlite:${_SERVER['DOCUMENT_ROOT']}/../dev-database.sqlite" const _DATABASE_USER = null; const _DATABASE_PASSWORD = null; -function _asset(string $assetURI): string -{ +function _asset(string $assetURI): string { global $front_url; return $front_url . "/" . $assetURI; } - - - diff --git a/profiles/prod-config-profile.php b/profiles/prod-config-profile.php index e185dfc..e9bb12c 100644 --- a/profiles/prod-config-profile.php +++ b/profiles/prod-config-profile.php @@ -19,4 +19,4 @@ function _asset(string $assetURI): string { // If the asset uri does not figure in the available assets array, // fallback to the uri itself. return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI); -} \ No newline at end of file +} diff --git a/public/api/index.php b/public/api/index.php index b6327e1..3ed5caa 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -37,6 +37,6 @@ http_response_code($response->getCode()); if ($response instanceof JsonHttpResponse) { header('Content-type: application/json'); echo $response->getJson(); -} else if ($response instanceof ViewHttpResponse) { +} elseif ($response instanceof ViewHttpResponse) { throw new Exception("API returned a view http response."); -} \ No newline at end of file +} diff --git a/public/front b/public/front new file mode 120000 index 0000000..c1394c9 --- /dev/null +++ b/public/front @@ -0,0 +1 @@ +../front \ No newline at end of file diff --git a/public/index.php b/public/index.php index 5141aa8..ea12e5f 100644 --- a/public/index.php +++ b/public/index.php @@ -16,4 +16,5 @@ $basePath = get_public_path(); $frontController = new FrontController($basePath); -$frontController->run(); \ No newline at end of file +$frontController->run(); + diff --git a/public/utils.php b/public/utils.php index a3566fe..7151386 100644 --- a/public/utils.php +++ b/public/utils.php @@ -3,18 +3,19 @@ /** * relative path of the public directory from the server's document root. */ -function get_public_path() { +function get_public_path(): string { // find the server path of the index.php file $basePath = substr(__DIR__, strlen($_SERVER['DOCUMENT_ROOT'])); $basePathLen = strlen($basePath); - if ($basePathLen == 0) + if ($basePathLen == 0) { return ""; - + } + $c = $basePath[$basePathLen - 1]; if ($c == "/" || $c == "\\") { $basePath = substr($basePath, 0, $basePathLen - 1); } return $basePath; -} \ No newline at end of file +} diff --git a/sql/.guard b/sql/.guard new file mode 100644 index 0000000..e69de29 diff --git a/sql/database.php b/sql/database.php index d49ddfd..8f5aa9d 100644 --- a/sql/database.php +++ b/sql/database.php @@ -25,6 +25,3 @@ function get_database(): PDO { return $pdo; } - - - diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 068c2e1..108b62a 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -1,9 +1,15 @@ -- drop tables here DROP TABLE IF EXISTS FormEntries; +DROP TABLE IF EXISTS AccountUser; DROP TABLE IF EXISTS TacticInfo; CREATE TABLE FormEntries(name varchar, description varchar); +CREATE TABLE AccountUser( + username varchar, + hash varchar, + email varchar unique +); CREATE TABLE TacticInfo( id integer PRIMARY KEY AUTOINCREMENT, diff --git a/src/Connexion.php b/src/Connexion.php index 788c0fb..987d35b 100644 --- a/src/Connexion.php +++ b/src/Connexion.php @@ -1,28 +1,27 @@ pdo = $pdo; } - public function lastInsertId() { + public function lastInsertId(): string { return $this->pdo->lastInsertId(); } /** * execute a request * @param string $query - * @param array $args + * @param array> $args * @return void */ public function exec(string $query, array $args) { @@ -33,8 +32,8 @@ class Connexion { /** * Execute a request, and return the returned rows * @param string $query the SQL request - * @param array $args an array containing the arguments label, value and type: ex: `[":label" => [$value, PDO::PARAM_TYPE]` - * @return array the returned rows of the request + * @param array> $args an array containing the arguments label, value and type: ex: `[":label" => [$value, PDO::PARAM_TYPE]` + * @return array[] the returned rows of the request */ public function fetch(string $query, array $args): array { $stmnt = $this->prepare($query, $args); @@ -42,6 +41,11 @@ class Connexion { return $stmnt->fetchAll(PDO::FETCH_ASSOC); } + /** + * @param string $query + * @param array> $args + * @return \PDOStatement + */ private function prepare(string $query, array $args): \PDOStatement { $stmnt = $this->pdo->prepare($query); foreach ($args as $name => $value) { @@ -50,4 +54,4 @@ class Connexion { return $stmnt; } -} \ No newline at end of file +} diff --git a/src/Controller/Api/APITacticController.php b/src/Controller/Api/APITacticController.php index f775ecf..ec0edc8 100644 --- a/src/Controller/Api/APITacticController.php +++ b/src/Controller/Api/APITacticController.php @@ -25,21 +25,21 @@ class APITacticController { public function updateName(int $tactic_id): HttpResponse { return Control::runChecked([ - "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()] + "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], ], function (HttpRequest $request) use ($tactic_id) { $this->model->updateName($tactic_id, $request["name"]); return HttpResponse::fromCode(HttpCodes::OK); - }); + }, true); } public function newTactic(): HttpResponse { return Control::runChecked([ - "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()] + "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], ], function (HttpRequest $request) { $tactic = $this->model->makeNew($request["name"]); $id = $tactic->getId(); return new JsonHttpResponse(["id" => $id]); - }); + }, true); } public function getTacticInfo(int $id): HttpResponse { @@ -52,4 +52,4 @@ class APITacticController { return new JsonHttpResponse($tactic_info); } -} \ No newline at end of file +} diff --git a/src/Controller/AuthController.php b/src/Controller/AuthController.php new file mode 100644 index 0000000..e42c27d --- /dev/null +++ b/src/Controller/AuthController.php @@ -0,0 +1,94 @@ +model = $model; + } + + public function displayRegister(): HttpResponse { + return ViewHttpResponse::twig("display_register.html.twig", []); + } + + /** + * @param string $viewName + * @param ValidationFail[] $fails + * @return HttpResponse + */ + private function displayBadFields(string $viewName, array $fails): HttpResponse { + $bad_fields = []; + foreach ($fails as $err) { + if ($err instanceof FieldValidationFail) { + $bad_fields[] = $err->getFieldName(); + } + } + return ViewHttpResponse::twig($viewName, ['bad_fields' => $bad_fields]); + } + + /** + * @param mixed[] $request + * @return HttpResponse + */ + public function confirmRegister(array $request): HttpResponse { + $fails = []; + $request = HttpRequest::from($request, $fails, [ + "username" => [Validators::name(), Validators::lenBetween(2, 32)], + "password" => [Validators::lenBetween(6, 256)], + "confirmpassword" => [Validators::lenBetween(6, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)], + ]); + if (!empty($fails)) { + return $this->displayBadFields("display_register.html.twig", $fails); + } + $fails = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email']); + if (empty($fails)) { + $results = $this->model->getUserFields($request['email']); + return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]); + } + return $this->displayBadFields("display_register.html.twig", $fails); + } + + + public function displayLogin(): HttpResponse { + return ViewHttpResponse::twig("display_login.html.twig", []); + } + + /** + * @param mixed[] $request + * @return HttpResponse + */ + public function confirmLogin(array $request): HttpResponse { + $fails = []; + $request = HttpRequest::from($request, $fails, [ + "password" => [Validators::lenBetween(6, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)], + ]); + if (!empty($fails)) { + return $this->displayBadFields("display_login.html.twig", $fails); + } + + $fails = $this->model->login($request['email'], $request['password']); + if (empty($fails)) { + $results = $this->model->getUserFields($request['email']); + return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]); + } + return $this->displayBadFields("display_login.html.twig", $fails); + } + +} diff --git a/src/Controller/Control.php b/src/Controller/Control.php index 8c00d2e..2b428d9 100644 --- a/src/Controller/Control.php +++ b/src/Controller/Control.php @@ -8,14 +8,16 @@ use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; use App\Validation\ValidationFail; +use App\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 callable $run the callback to run if the request is valid according to the given schema. + * @param array $schema an array of `fieldName => Validators` 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, in case of errors, will be a JsonHttpResponse, instead + * of the ViewHttpResponse for an error view. * @return HttpResponse */ public static function runChecked(array $schema, callable $run, bool $errorInJson): HttpResponse { @@ -23,7 +25,7 @@ class Control { $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) { + if ($errorInJson) { return new JsonHttpResponse([$fail, HttpCodes::BAD_REQUEST]); } return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST); @@ -34,10 +36,12 @@ 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 callable $run the callback to run if the request is valid according to 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 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, in case of errors, will be a JsonHttpResponse, instead + * of the ViewHttpResponse for an error view. * @return HttpResponse */ public static function runCheckedFrom(array $data, array $schema, callable $run, bool $errorInJson): HttpResponse { @@ -45,7 +49,7 @@ class Control { $request = HttpRequest::from($data, $fails, $schema); if (!empty($fails)) { - if($errorInJson) { + if ($errorInJson) { return new JsonHttpResponse($fails, HttpCodes::BAD_REQUEST); } return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST); @@ -53,9 +57,6 @@ class Control { return call_user_func_array($run, [$request]); } - - - -} \ No newline at end of file +} diff --git a/src/Controller/EditorController.php b/src/Controller/EditorController.php index 2dd2cbb..32a0fd5 100644 --- a/src/Controller/EditorController.php +++ b/src/Controller/EditorController.php @@ -13,7 +13,6 @@ use App\Http\ViewHttpResponse; use App\Model\TacticModel; class EditorController { - private TacticModel $model; /** @@ -47,4 +46,4 @@ class EditorController { return $this->openEditor($tactic); } -} \ No newline at end of file +} diff --git a/src/Controller/ErrorController.php b/src/Controller/ErrorController.php index e91d05f..7fc5239 100644 --- a/src/Controller/ErrorController.php +++ b/src/Controller/ErrorController.php @@ -3,17 +3,23 @@ namespace App\Controller; require_once __DIR__ . "/../react-display.php"; -use \Twig\Environment; + +use App\Validation\ValidationFail; +use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; -class ErrorController -{ - public static function displayFailures(array $failures, Environment $twig) { +class ErrorController { + /** + * @param ValidationFail[] $failures + * @param Environment $twig + * @return void + */ + public static function displayFailures(array $failures, Environment $twig): void { try { $twig->display("error.html.twig", ['failures' => $failures]); - } catch (LoaderError | RuntimeError | SyntaxError $e) { + } catch (LoaderError|RuntimeError|SyntaxError $e) { echo "Twig error: $e"; } } diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index 991d5c9..491323e 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -2,35 +2,23 @@ namespace App\Controller; -use App\Controller; -use App\Connexion; -use App\Controller\UserController; - use AltoRouter; -use App\Controller\ErrorController; use App\Gateway\FormResultGateway; -use App\Gateway\TacticInfoGateway; use App\Http\HttpCodes; use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; -use App\Model\TacticModel; -use App\Validation\ValidationFail; use Exception; use Twig\Loader\FilesystemLoader; -class FrontController{ +class FrontController { private AltoRouter $router; - private array $dictControllerRole; public function __construct(string $basePath) { $this->router = $this->createRouter($basePath); - $this->dictControllerRole = [ - "UserController" => "public", - "EditorController" => "public" - ]; + $this->initializeRouterMap(); } /** @@ -38,11 +26,11 @@ class FrontController{ * * @return void */ - public function run() : void { + public function run(): void { $this->initializeRouterMap(); - $match = $this->router->match(); - if ($match != null){ + $match = $this->router->match(); + if ($match != null) { $this->handleMatch($match); } else { $this->diplayViewByKind(ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND)); @@ -57,7 +45,7 @@ class FrontController{ * @param string $basePath * @return AltoRouter */ - public function createRouter(string $basePath) : AltoRouter { + public function createRouter(string $basePath): AltoRouter { $router = new AltoRouter(); $router->setBasePath($basePath); return $router; @@ -68,7 +56,7 @@ class FrontController{ * * @return void */ - private function initializeRouterMap() : void { + private function initializeRouterMap(): void { $this->router->map("GET", "/", "UserController"); $this->router->map("GET", "/[a:action]?", "UserController"); $this->router->map("GET", "/tactic/[a:action]/[i:idTactic]?", "EditorController"); @@ -79,11 +67,11 @@ class FrontController{ } /** - * Call + * Call * * @return ViewHttpResponse */ - private function handleMatch($match){ + private function handleMatch($match) { $tag = $match['target']; $action = $this->getAction($match); @@ -94,29 +82,28 @@ class FrontController{ // foreach ($key, $value : ) // } - private function tryToCall($controller, $action, array $params){ + private function tryToCall($controller, $action, array $params) { unset($params["action"]); $controller = $this->initControllerByRole($controller); try { - if (is_callable(array($controller, $action))){ + if (is_callable(array($controller, $action))) { return call_user_func_array(array($controller, $action), $params); } else { return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); } - } - catch (Exception $e) { + } catch (Exception $e) { return ViewHttpResponse::twig("error.html.twig", [], HttpCodes::NOT_FOUND); } } - + /** * Get the right method to call to do an action * * @param array $match * @return string */ - private function getAction(array $match) : string { - if (isset($match["params"]["action"])){ + private function getAction(array $match): string { + if (isset($match["params"]["action"])) { return $match["params"]["action"]; } return "home"; @@ -129,14 +116,14 @@ class FrontController{ * @return void */ private function initControllerByRole(string $controller) { - + $index = $controller; $namespace = "\\App\\Controller\\"; - $controller = $namespace.$controller; + $controller = $namespace . $controller; + - - if (isset($_SESSION['role'])){ - if ($_SESSION['role'] == $this->dictControllerRole[$index]){ + if (isset($_SESSION['role'])) { + if ($_SESSION['role'] == $this->dictControllerRole[$index]) { $controller = new $controller(); return $controller; } @@ -160,7 +147,7 @@ class FrontController{ * @param array $match * @return void */ - private function handleResponseByType(HttpResponse $response) : void { + private function handleResponseByType(HttpResponse $response): void { // $response = call_user_func_array($match['target'], $match['params']); http_response_code($response->getCode()); if ($response instanceof ViewHttpResponse) { @@ -179,7 +166,7 @@ class FrontController{ * @param ViewHttpResponse $response * @return void */ - private function diplayViewByKind(ViewHttpResponse $response) : void { + private function diplayViewByKind(ViewHttpResponse $response): void { $file = $response->getFile(); $args = $response->getArguments(); @@ -192,12 +179,12 @@ class FrontController{ $loader = new FilesystemLoader('../src/Views/'); $twig = new \Twig\Environment($loader); $twig->display($file, $args); - } catch (\Twig\Error\RuntimeError | \Twig\Error\SyntaxError $e) { + } catch (\Twig\Error\RuntimeError|\Twig\Error\SyntaxError $e) { http_response_code(500); echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); throw $e; } - break; + break; } } } \ No newline at end of file diff --git a/src/Controller/SampleFormController.php b/src/Controller/SampleFormController.php new file mode 100644 index 0000000..773a4db --- /dev/null +++ b/src/Controller/SampleFormController.php @@ -0,0 +1,64 @@ +gateway = $gateway; + } + + + public function displayFormReact(): HttpResponse { + return ViewHttpResponse::react("views/SampleForm.tsx", []); + } + + public function displayFormTwig(): HttpResponse { + return ViewHttpResponse::twig('sample_form.html.twig', []); + } + + /** + * @param array $form + * @param callable(array>): ViewHttpResponse $response + * @return HttpResponse + */ + private function submitForm(array $form, callable $response): HttpResponse { + return Control::runCheckedFrom($form, [ + "name" => [Validators::lenBetween(0, 32), Validators::name("Le nom ne peut contenir que des lettres, des chiffres et des accents")], + "description" => [Validators::lenBetween(0, 512)], + ], function (HttpRequest $req) use ($response) { + $description = htmlspecialchars($req["description"]); + $this->gateway->insert($req["name"], $description); + $results = ["results" => $this->gateway->listResults()]; + return call_user_func_array($response, [$results]); + }, false); + } + + /** + * @param array $form + * @return HttpResponse + */ + public function submitFormTwig(array $form): HttpResponse { + return $this->submitForm($form, fn(array $results) => ViewHttpResponse::twig('display_results.html.twig', $results)); + } + + /** + * @param array $form + * @return HttpResponse + */ + public function submitFormReact(array $form): HttpResponse { + return $this->submitForm($form, fn(array $results) => ViewHttpResponse::react('views/DisplayResults.tsx', $results)); + } +} diff --git a/src/Controller/VisualizerController.php b/src/Controller/VisualizerController.php new file mode 100644 index 0000000..3c8b55e --- /dev/null +++ b/src/Controller/VisualizerController.php @@ -0,0 +1,32 @@ +tacticModel = $tacticModel; + } + + public function openVisualizer(int $id): HttpResponse { + $tactic = $this->tacticModel->get($id); + + if ($tactic == null) { + return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND); + } + + return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]); + + } +} diff --git a/src/Data/Account.php b/src/Data/Account.php index 155f2ae..2a21bf1 100755 --- a/src/Data/Account.php +++ b/src/Data/Account.php @@ -4,7 +4,7 @@ namespace App\Data; use http\Exception\InvalidArgumentException; -const PHONE_NUMBER_REGEXP = "\\+[0-9]+"; +const PHONE_NUMBER_REGEXP = "/^\\+[0-9]+$/"; /** * Base class of a user account. @@ -29,7 +29,7 @@ class Account { private AccountUser $user; /** - * @var array account's teams + * @var Team[] account's teams */ private array $teams; @@ -38,10 +38,13 @@ class Account { */ private int $id; + /** * @param string $email * @param string $phoneNumber * @param AccountUser $user + * @param Team[] $teams + * @param int $id */ public function __construct(string $email, string $phoneNumber, AccountUser $user, array $teams, int $id) { $this->email = $email; @@ -79,7 +82,7 @@ class Account { * @param string $phoneNumber */ public function setPhoneNumber(string $phoneNumber): void { - if (!filter_var($phoneNumber, FILTER_VALIDATE_REGEXP, PHONE_NUMBER_REGEXP)) { + if (!preg_match(PHONE_NUMBER_REGEXP, $phoneNumber)) { throw new InvalidArgumentException("Invalid phone number"); } $this->phoneNumber = $phoneNumber; @@ -89,6 +92,9 @@ class Account { return $this->id; } + /** + * @return Team[] + */ public function getTeams(): array { return $this->teams; } @@ -96,4 +102,4 @@ class Account { public function getUser(): AccountUser { return $this->user; } -} \ No newline at end of file +} diff --git a/src/Data/AccountUser.php b/src/Data/AccountUser.php index 7808062..5d3b497 100755 --- a/src/Data/AccountUser.php +++ b/src/Data/AccountUser.php @@ -36,17 +36,17 @@ class AccountUser implements User { return $this->age; } - public function setName(string $name) { + public function setName(string $name): void { $this->name = $name; } - public function setProfilePicture(Url $profilePicture) { + public function setProfilePicture(Url $profilePicture): void { $this->profilePicture = $profilePicture; } - public function setAge(int $age) { + public function setAge(int $age): void { $this->age = $age; } -} \ No newline at end of file +} diff --git a/src/Data/Color.php b/src/Data/Color.php index f841731..0b1fbb3 100755 --- a/src/Data/Color.php +++ b/src/Data/Color.php @@ -14,7 +14,7 @@ class Color { * @param int $value 6 bytes unsigned int that represents an RGB color * @throws \InvalidArgumentException if the value is negative or greater than 0xFFFFFF */ - public function __constructor(int $value) { + public function __construct(int $value) { if ($value < 0 || $value > 0xFFFFFF) { throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF"); } @@ -27,4 +27,4 @@ class Color { public function getValue(): int { return $this->value; } -} \ No newline at end of file +} diff --git a/src/Data/Member.php b/src/Data/Member.php index 91b09c4..6500733 100755 --- a/src/Data/Member.php +++ b/src/Data/Member.php @@ -39,4 +39,4 @@ class Member { return $this->role; } -} \ No newline at end of file +} diff --git a/src/Data/MemberRole.php b/src/Data/MemberRole.php index 05d746d..a1d4d31 100755 --- a/src/Data/MemberRole.php +++ b/src/Data/MemberRole.php @@ -2,7 +2,6 @@ namespace App\Data; - use http\Exception\InvalidArgumentException; /** @@ -37,4 +36,4 @@ final class MemberRole { return ($this->value == self::ROLE_COACH); } -} \ No newline at end of file +} diff --git a/src/Data/TacticInfo.php b/src/Data/TacticInfo.php index 901280d..eef7bb3 100644 --- a/src/Data/TacticInfo.php +++ b/src/Data/TacticInfo.php @@ -30,7 +30,10 @@ class TacticInfo implements \JsonSerializable { return $this->creation_date; } - public function jsonSerialize() { + /** + * @return array + */ + public function jsonSerialize(): array { return get_object_vars($this); } -} \ No newline at end of file +} diff --git a/src/Data/Team.php b/src/Data/Team.php index 48643d9..aca4e0d 100755 --- a/src/Data/Team.php +++ b/src/Data/Team.php @@ -11,7 +11,7 @@ class Team { private Color $secondColor; /** - * @var array maps users with their role + * @var Member[] maps users with their role */ private array $members; @@ -20,7 +20,7 @@ class Team { * @param Url $picture * @param Color $mainColor * @param Color $secondColor - * @param array $members + * @param Member[] $members */ public function __construct(string $name, Url $picture, Color $mainColor, Color $secondColor, array $members) { $this->name = $name; @@ -58,8 +58,11 @@ class Team { return $this->secondColor; } + /** + * @return Member[] + */ public function listMembers(): array { - return array_map(fn ($id, $role) => new Member($id, $role), $this->members); + return $this->members; } -} \ No newline at end of file +} diff --git a/src/Data/User.php b/src/Data/User.php index 15c9995..6cb55c2 100755 --- a/src/Data/User.php +++ b/src/Data/User.php @@ -4,7 +4,6 @@ namespace App\Data; use http\Url; - /** * Public information about a user */ @@ -24,4 +23,4 @@ interface User { * @return int The user's age */ public function getAge(): int; -} \ No newline at end of file +} diff --git a/src/Gateway/AuthGateway.php b/src/Gateway/AuthGateway.php new file mode 100644 index 0000000..5acc01c --- /dev/null +++ b/src/Gateway/AuthGateway.php @@ -0,0 +1,47 @@ +con = $con; + } + + + public function mailExist(string $email): bool { + return $this->getUserFields($email) != null; + } + + + public function insertAccount(string $username, string $hash, string $email): void { + $this->con->exec("INSERT INTO AccountUser VALUES (:username,:hash,:email)", [':username' => [$username, PDO::PARAM_STR],':hash' => [$hash, PDO::PARAM_STR],':email' => [$email, PDO::PARAM_STR]]); + } + + public function getUserHash(string $email): string { + $results = $this->con->fetch("SELECT hash FROM AccountUser WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]); + return $results[0]['hash']; + } + + + /** + * @param string $email + * @return array|null + */ + public function getUserFields(string $email): ?array { + $results = $this->con->fetch("SELECT username,email FROM AccountUser WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]); + $firstRow = $results[0] ?? null; + return $firstRow; + } + + + + +} diff --git a/src/Gateway/FormResultGateway.php b/src/Gateway/FormResultGateway.php new file mode 100644 index 0000000..36178ad --- /dev/null +++ b/src/Gateway/FormResultGateway.php @@ -0,0 +1,35 @@ +con = $con; + } + + + public function insert(string $username, string $description): void { + $this->con->exec( + "INSERT INTO FormEntries VALUES (:name, :description)", + [ + ":name" => [$username, PDO::PARAM_STR], + "description" => [$description, PDO::PARAM_STR], + ] + ); + } + + /** + * @return array + */ + public function listResults(): array { + return $this->con->fetch("SELECT * FROM FormEntries", []); + } +} diff --git a/src/Gateway/TacticInfoGateway.php b/src/Gateway/TacticInfoGateway.php index 20d2957..3441c9a 100644 --- a/src/Gateway/TacticInfoGateway.php +++ b/src/Gateway/TacticInfoGateway.php @@ -4,7 +4,7 @@ namespace App\Gateway; use App\Connexion; use App\Data\TacticInfo; -use \PDO; +use PDO; class TacticInfoGateway { private Connexion $con; @@ -43,14 +43,14 @@ class TacticInfoGateway { return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"])); } - public function updateName(int $id, string $name) { + public function updateName(int $id, string $name): void { $this->con->exec( "UPDATE TacticInfo SET name = :name WHERE id = :id", [ ":name" => [$name, PDO::PARAM_STR], - ":id" => [$id, PDO::PARAM_INT] + ":id" => [$id, PDO::PARAM_INT], ] ); } -} \ No newline at end of file +} diff --git a/src/Http/HttpCodes.php b/src/Http/HttpCodes.php index b41af8a..f9d550c 100644 --- a/src/Http/HttpCodes.php +++ b/src/Http/HttpCodes.php @@ -10,4 +10,4 @@ class HttpCodes { public const BAD_REQUEST = 400; public const NOT_FOUND = 404; -} \ No newline at end of file +} diff --git a/src/Http/HttpRequest.php b/src/Http/HttpRequest.php index f841752..d199227 100644 --- a/src/Http/HttpRequest.php +++ b/src/Http/HttpRequest.php @@ -4,12 +4,23 @@ namespace App\Http; use App\Validation\FieldValidationFail; use App\Validation\Validation; +use App\Validation\ValidationFail; +use App\Validation\Validator; use ArrayAccess; use Exception; +/** + * @implements ArrayAccess + * */ class HttpRequest implements ArrayAccess { + /** + * @var array + */ private array $data; + /** + * @param array $data + */ private function __construct(array $data) { $this->data = $data; } @@ -17,9 +28,9 @@ class HttpRequest implements ArrayAccess { /** * Creates a new HttpRequest instance, and ensures that the given request data validates the given schema. * This is a simple function that only supports flat schemas (non-composed, the data must only be a k/v array pair.) - * @param array $request the request's data - * @param array $fails a reference to a failure array, that will contain the reported validation failures. - * @param array $schema the schema to satisfy. a schema is a simple array with a string key (which is the top-level field name), and a set of validators + * @param array $request the request's data + * @param array $fails a reference to a failure array, that will contain the reported validation failures. + * @param array $schema the schema to satisfy. a schema is a simple array with a string key (which is the top-level field name), and a set of validators * @return HttpRequest|null the built HttpRequest instance, or null if a field is missing, or if any of the schema validator failed */ public static function from(array $request, array &$fails, array $schema): ?HttpRequest { @@ -43,15 +54,28 @@ class HttpRequest implements ArrayAccess { return isset($this->data[$offset]); } + /** + * @param $offset + * @return mixed + */ public function offsetGet($offset) { return $this->data[$offset]; } + /** + * @param $offset + * @param $value + * @throws Exception + */ public function offsetSet($offset, $value) { throw new Exception("requests are immutable objects."); } + /** + * @param $offset + * @throws Exception + */ public function offsetUnset($offset) { throw new Exception("requests are immutable objects."); } -} \ No newline at end of file +} diff --git a/src/Http/HttpResponse.php b/src/Http/HttpResponse.php index 9f081a5..5d8c3bf 100644 --- a/src/Http/HttpResponse.php +++ b/src/Http/HttpResponse.php @@ -3,7 +3,6 @@ namespace App\Http; class HttpResponse { - private int $code; /** @@ -21,4 +20,4 @@ class HttpResponse { return new HttpResponse($code); } -} \ No newline at end of file +} diff --git a/src/Http/JsonHttpResponse.php b/src/Http/JsonHttpResponse.php index 9d7423f..bbd3d80 100644 --- a/src/Http/JsonHttpResponse.php +++ b/src/Http/JsonHttpResponse.php @@ -3,7 +3,6 @@ namespace App\Http; class JsonHttpResponse extends HttpResponse { - /** * @var mixed Any JSON serializable value */ @@ -26,4 +25,4 @@ class JsonHttpResponse extends HttpResponse { return $result; } -} \ No newline at end of file +} diff --git a/src/Http/ViewHttpResponse.php b/src/Http/ViewHttpResponse.php index 0e92054..2e517d7 100644 --- a/src/Http/ViewHttpResponse.php +++ b/src/Http/ViewHttpResponse.php @@ -3,7 +3,6 @@ namespace App\Http; class ViewHttpResponse extends HttpResponse { - public const TWIG_VIEW = 0; public const REACT_VIEW = 1; @@ -12,7 +11,7 @@ class ViewHttpResponse extends HttpResponse { */ private string $file; /** - * @var array View arguments + * @var array View arguments */ private array $arguments; /** @@ -24,7 +23,7 @@ class ViewHttpResponse extends HttpResponse { * @param int $code * @param int $kind * @param string $file - * @param array $arguments + * @param array $arguments */ private function __construct(int $kind, string $file, array $arguments, int $code = HttpCodes::OK) { parent::__construct($code); @@ -41,6 +40,9 @@ class ViewHttpResponse extends HttpResponse { return $this->file; } + /** + * @return array + */ public function getArguments(): array { return $this->arguments; } @@ -48,7 +50,7 @@ class ViewHttpResponse extends HttpResponse { /** * Create a twig view response * @param string $file - * @param array $arguments + * @param array $arguments * @param int $code * @return ViewHttpResponse */ @@ -59,7 +61,7 @@ class ViewHttpResponse extends HttpResponse { /** * Create a react view response * @param string $file - * @param array $arguments + * @param array $arguments * @param int $code * @return ViewHttpResponse */ @@ -67,4 +69,4 @@ class ViewHttpResponse extends HttpResponse { return new ViewHttpResponse(self::REACT_VIEW, $file, $arguments, $code); } -} \ No newline at end of file +} diff --git a/src/Model/AuthModel.php b/src/Model/AuthModel.php new file mode 100644 index 0000000..45b63e4 --- /dev/null +++ b/src/Model/AuthModel.php @@ -0,0 +1,80 @@ +gateway = $gateway; + } + + + /** + * @param string $username + * @param string $password + * @param string $confirmPassword + * @param string $email + * @return ValidationFail[] + */ + public function register(string $username, string $password, string $confirmPassword, string $email): array { + $errors = []; + + if ($password != $confirmPassword) { + $errors[] = new FieldValidationFail("confirmpassword", "password and password confirmation are not equals"); + } + + if ($this->gateway->mailExist($email)) { + $errors[] = new FieldValidationFail("email", "email already exist"); + } + + if(empty($errors)) { + $hash = password_hash($password, PASSWORD_DEFAULT); + $this->gateway->insertAccount($username, $hash, $email); + } + + return $errors; + } + + /** + * @param string $email + * @return array|null + */ + public function getUserFields(string $email): ?array { + return $this->gateway->getUserFields($email); + } + + + /** + * @param string $email + * @param string $password + * @return ValidationFail[] $errors + */ + public function login(string $email, string $password): array { + $errors = []; + + if (!$this->gateway->mailExist($email)) { + $errors[] = new FieldValidationFail("email", "email doesnt exists"); + return $errors; + } + $hash = $this->gateway->getUserHash($email); + + if (!password_verify($password, $hash)) { + $errors[] = new FieldValidationFail("password", "invalid password"); + } + + return $errors; + } + + + + + +} diff --git a/src/Model/TacticModel.php b/src/Model/TacticModel.php index c0b1ffe..cabcdec 100644 --- a/src/Model/TacticModel.php +++ b/src/Model/TacticModel.php @@ -6,7 +6,6 @@ use App\Data\TacticInfo; use App\Gateway\TacticInfoGateway; class TacticModel { - public const TACTIC_DEFAULT_NAME = "Nouvelle tactique"; @@ -40,7 +39,7 @@ class TacticModel { * Update the name of a tactic * @param int $id the tactic identifier * @param string $name the new name to set - * @return true if the update was done successfully + * @return bool true if the update was done successfully */ public function updateName(int $id, string $name): bool { if ($this->tactics->get($id) == null) { @@ -51,4 +50,4 @@ class TacticModel { return true; } -} \ No newline at end of file +} diff --git a/src/Validation/ComposedValidator.php b/src/Validation/ComposedValidator.php index 418b1ed..cc6e9e5 100644 --- a/src/Validation/ComposedValidator.php +++ b/src/Validation/ComposedValidator.php @@ -3,7 +3,6 @@ namespace App\Validation; class ComposedValidator extends Validator { - private Validator $first; private Validator $then; @@ -21,4 +20,4 @@ class ComposedValidator extends Validator { $thenFailures = $this->then->validate($name, $val); return array_merge($firstFailures, $thenFailures); } -} \ No newline at end of file +} diff --git a/src/Validation/FieldValidationFail.php b/src/Validation/FieldValidationFail.php index 5b535f7..af2ce3a 100644 --- a/src/Validation/FieldValidationFail.php +++ b/src/Validation/FieldValidationFail.php @@ -2,7 +2,6 @@ namespace App\Validation; - /** * An error that concerns a field, with a bound message name */ @@ -34,7 +33,10 @@ class FieldValidationFail extends ValidationFail { return new FieldValidationFail($fieldName, "field is missing"); } - public function jsonSerialize() { + /** + * @return array + */ + public function jsonSerialize(): array { return ["field" => $this->fieldName, "message" => $this->getMessage()]; } -} \ No newline at end of file +} diff --git a/src/Validation/FunctionValidator.php b/src/Validation/FunctionValidator.php index 6874d63..4949d8b 100644 --- a/src/Validation/FunctionValidator.php +++ b/src/Validation/FunctionValidator.php @@ -3,11 +3,13 @@ namespace App\Validation; class FunctionValidator extends Validator { - + /** + * @var callable(string, mixed): ValidationFail[] + */ private $validate_fn; /** - * @param callable $validate_fn the validate function. Must have the same signature as the {@link Validator::validate()} method. + * @param callable(string, mixed): ValidationFail[] $validate_fn the validate function. Must have the same signature as the {@link Validator::validate()} method. */ public function __construct(callable $validate_fn) { $this->validate_fn = $validate_fn; @@ -16,4 +18,4 @@ class FunctionValidator extends Validator { public function validate(string $name, $val): array { return call_user_func_array($this->validate_fn, [$name, $val]); } -} \ No newline at end of file +} diff --git a/src/Validation/SimpleFunctionValidator.php b/src/Validation/SimpleFunctionValidator.php index 079452d..cec52c0 100644 --- a/src/Validation/SimpleFunctionValidator.php +++ b/src/Validation/SimpleFunctionValidator.php @@ -6,13 +6,18 @@ namespace App\Validation; * A simple validator that takes a predicate and an error factory */ class SimpleFunctionValidator extends Validator { - + /** + * @var callable(mixed): bool + */ private $predicate; + /** + * @var callable(string): ValidationFail[] + */ private $errorFactory; /** - * @param callable $predicate a function predicate with signature: `(string) => bool`, to validate the given string - * @param callable $errorsFactory a factory function with signature `(string) => array` to emit failures when the predicate fails + * @param callable(mixed): bool $predicate a function predicate with signature: `(string) => bool`, to validate the given string + * @param callable(string): ValidationFail[] $errorsFactory a factory function with signature `(string) => array` to emit failures when the predicate fails */ public function __construct(callable $predicate, callable $errorsFactory) { $this->predicate = $predicate; @@ -25,4 +30,4 @@ class SimpleFunctionValidator extends Validator { } return []; } -} \ No newline at end of file +} diff --git a/src/Validation/Validation.php b/src/Validation/Validation.php index 4372380..f1392e6 100644 --- a/src/Validation/Validation.php +++ b/src/Validation/Validation.php @@ -6,12 +6,11 @@ namespace App\Validation; * Utility class for validation */ class Validation { - /** * Validate a value from validators, appending failures in the given errors array. * @param mixed $val the value to validate * @param string $valName the name of the value - * @param array $failures array to push when a validator fails + * @param ValidationFail[] $failures array to push when a validator fails * @param Validator ...$validators given validators * @return bool true if any of the given validators did fail */ @@ -27,4 +26,4 @@ class Validation { return $had_errors; } -} \ No newline at end of file +} diff --git a/src/Validation/ValidationFail.php b/src/Validation/ValidationFail.php index fa5139c..4f1ec22 100644 --- a/src/Validation/ValidationFail.php +++ b/src/Validation/ValidationFail.php @@ -2,7 +2,9 @@ namespace App\Validation; -class ValidationFail implements \JsonSerializable { +use JsonSerializable; + +class ValidationFail implements JsonSerializable { private string $kind; private string $message; @@ -24,7 +26,10 @@ class ValidationFail implements \JsonSerializable { return $this->kind; } - public function jsonSerialize() { + /** + * @return array + */ + public function jsonSerialize(): array { return ["error" => $this->kind, "message" => $this->message]; } @@ -32,4 +37,4 @@ class ValidationFail implements \JsonSerializable { return new ValidationFail("not found", $message); } -} \ No newline at end of file +} diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index 6cdafb9..8227e46 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -3,14 +3,13 @@ namespace App\Validation; abstract class Validator { - /** * validates a variable string * @param string $name the name of the tested value * @param mixed $val the value to validate - * @return array the errors the validator has reported + * @return ValidationFail[] the errors the validator has reported */ - public abstract function validate(string $name, $val): array; + abstract public function validate(string $name, $val): array; /** * Creates a validator composed of this validator, and given validator @@ -21,4 +20,4 @@ abstract class Validator { return new ComposedValidator($this, $other); } -} \ No newline at end of file +} diff --git a/src/Validation/Validators.php b/src/Validation/Validators.php index ea9da46..94aea23 100644 --- a/src/Validation/Validators.php +++ b/src/Validation/Validators.php @@ -6,11 +6,10 @@ namespace App\Validation; * A collection of standard validators */ class Validators { - /** * @return Validator a validator that validates a given regex */ - public static function regex(string $regex, string $msg = null): Validator { + public static function regex(string $regex, ?string $msg = null): Validator { return new SimpleFunctionValidator( fn(string $str) => preg_match($regex, $str), fn(string $name) => [new FieldValidationFail($name, $msg == null ? "field does not validates pattern $regex" : $msg)] @@ -20,7 +19,7 @@ class Validators { /** * @return Validator a validator that validates strings that only contains numbers, letters, accents letters, `-` and `_`. */ - public static function name($msg = null): Validator { + public static function name(?string $msg = null): Validator { return self::regex("/^[0-9a-zA-Zà-üÀ-Ü_-]*$/", $msg); } @@ -51,4 +50,4 @@ class Validators { } ); } -} \ No newline at end of file +} diff --git a/src/Views/display_auth_confirm.html.twig b/src/Views/display_auth_confirm.html.twig new file mode 100644 index 0000000..60c63b2 --- /dev/null +++ b/src/Views/display_auth_confirm.html.twig @@ -0,0 +1,46 @@ + + + + + Profil Utilisateur + + + + + + + + \ No newline at end of file diff --git a/src/Views/display_login.html.twig b/src/Views/display_login.html.twig new file mode 100644 index 0000000..33b2385 --- /dev/null +++ b/src/Views/display_login.html.twig @@ -0,0 +1,85 @@ + + + + Connexion + + + + + +
+

Se connecter

+
+
+ + + + + + +
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/src/Views/display_register.html.twig b/src/Views/display_register.html.twig new file mode 100644 index 0000000..40199a0 --- /dev/null +++ b/src/Views/display_register.html.twig @@ -0,0 +1,88 @@ + + + + S'enregistrer + + + + + +
+

S'enregistrer

+
+
+ + + + + + + + + +
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/src/Views/display_results.html.twig b/src/Views/display_results.html.twig index 6d2aef0..a33546b 100644 --- a/src/Views/display_results.html.twig +++ b/src/Views/display_results.html.twig @@ -1,4 +1,3 @@ - @@ -14,5 +13,6 @@

description: {{ v.description }}

{% endfor %} + \ No newline at end of file diff --git a/src/Views/sample_form.html.twig b/src/Views/sample_form.html.twig new file mode 100644 index 0000000..6f4a9b5 --- /dev/null +++ b/src/Views/sample_form.html.twig @@ -0,0 +1,19 @@ + + + + Twig view + + + +

Hello, this is a sample form made in Twig !

+ +
+ + + + + +
+ + + \ No newline at end of file diff --git a/src/react-display.php b/src/react-display.php index b965a3a..5baf41b 100644 --- a/src/react-display.php +++ b/src/react-display.php @@ -3,11 +3,11 @@ /** * sends a react view to the user client. * @param string $url url of the react file to render - * @param array $arguments arguments to pass to the rendered react component - * The arguments must be a json-encodable key/value dictionary. + * @param array $arguments arguments to pass to the rendered react component + * The arguments must be a json-encodable key/value dictionary. * @return void */ function send_react_front(string $url, array $arguments) { // the $url and $argument values are used into the included file require_once "react-display-file.php"; -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index 9da1fb5..d01f3cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "dom.iterable", "esnext" ], - "types": ["vite/client"], + "types": ["vite/client", "vite-plugin-svgr/client"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, diff --git a/verify.sh b/verify.sh new file mode 100755 index 0000000..314b8bc --- /dev/null +++ b/verify.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +## verify php and typescript types + +echo "running php typechecking" +vendor/bin/phpstan analyze && echo "php types are respected" + +echo "running typescript typechecking" +npm run tsc && echo "typescript types are respected" \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 03ab8f4..4ff1dc5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import {defineConfig} from "vite"; import react from '@vitejs/plugin-react'; import fs from "fs"; import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; +import svgr from "vite-plugin-svgr"; function resolve_entries(dirname: string): [string, string][] { @@ -38,6 +39,9 @@ export default defineConfig({ react(), cssInjectedByJsPlugin({ relativeCSSInjection: true, + }), + svgr({ + include: "**/*.svg?react" }) ] })