diff --git a/.github/workflows/back-pr.yaml b/.github/workflows/back-pr.yaml index 0b4e757..cffc1f6 100644 --- a/.github/workflows/back-pr.yaml +++ b/.github/workflows/back-pr.yaml @@ -1,49 +1,58 @@ -name: Backend on Pull Request +name: On Pull Request on: pull_request: branches: - main - develop - paths: "back/**" + paths: "src/**" types: [opened, synchronize, reopened] jobs: tests: name: Tests runs-on: ubuntu-latest + env: + DB_DATABASE: PoolNET + DB_USER: root + DB_PASSWORD: root + steps: - - name: Checkout + - name: Checkout repository uses: actions/checkout@v2 with: fetch-depth: 0 + - name: Build and seed database + run: | + cd ${{ github.workspace }}/src/config/database + sqlite3 ${{ env.DB_DATABASE }}.db < database.sql + sqlite3 ${{ env.DB_DATABASE }}.db < db_seed_test.sql + - name: Rename env-example to env + run: | + cd ${{ github.workspace }}/src/config + mv env-example.php Env.php - name: Setup PHP with Xdebug uses: shivammathur/setup-php@v2 with: php-version: "8.1" coverage: xdebug - - name: Install dependencies with composer + - name: Install dependencies with composer and run autoload run: | - cd ${{ github.workspace }}/back + cd ${{ github.workspace }}/src composer update --no-ansi --no-interaction --no-progress - - name: Rename env-example to env - run: | - cd ${{ github.workspace }}/back/config - mv env-example.php Env.php - cd ${{ github.workspace }}/back - composer dump-autoload + cd ${{ github.workspace }}/src + composer dump-autoload -o - name: Run tests with phpunit/phpunit run: | - cd ${{ github.workspace }}/back + cd ${{ github.workspace }}/src vendor/bin/phpunit __tests__ --coverage-clover=coverage.xml --log-junit=execution.xml # - name: Fix code coverage paths # run: | - # sed -i 's@'$GITHUB_WORKSPACE'@/home/runner/work/PoolNET/PoolNET@g' ${{ github.workspace }}/back/coverage.xml - # sed -i 's@'$GITHUB_WORKSPACE'@/home/runner/work/PoolNET/PoolNET@g' ${{ github.workspace }}/back/execution.xml + # sed -i 's@'$GITHUB_WORKSPACE'@/home/runner/work/PoolNET/PoolNET@g' ${{ github.workspace }}/src/coverage.xml + # sed -i 's@'$GITHUB_WORKSPACE'@/home/runner/work/PoolNET/PoolNET@g' ${{ github.workspace }}/src/execution.xml - name: See coverage run: | - cd ${{ github.workspace }}/back + cd ${{ github.workspace }}/src cat coverage.xml - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v5 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} diff --git a/back/.gitignore b/.gitignore similarity index 58% rename from back/.gitignore rename to .gitignore index 4349676..2e0faa3 100644 --- a/back/.gitignore +++ b/.gitignore @@ -1,14 +1,19 @@ composer.phar -/vendor/ +/src/vendor/ node_modules .vscode .phpunit.cache +.coverage # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file # composer.lock -/config/env.php -/config/env-sastre.php -/__tests__/E2E/constants.ts \ No newline at end of file +.htaccess +/src/config/env.php +/src/config/env-sastre.php +/src/config/database/PoolNET.db +/src/config/database/db_seed_prod* +/src/config/database/db_init_local.sh +/src/__tests__/E2E/constants.ts \ No newline at end of file diff --git a/.htaccess.example b/.htaccess.example new file mode 100644 index 0000000..c11348c --- /dev/null +++ b/.htaccess.example @@ -0,0 +1,8 @@ +RewriteEngine On +RewriteBase /PoolNET/ + +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_URI} !^(css|img) + +RewriteRule ^(.*)$ index.php [QSA,L] \ No newline at end of file diff --git a/back/__tests__/models/JwtHandlerTest.php b/back/__tests__/models/JwtHandlerTest.php deleted file mode 100644 index 219e1f4..0000000 --- a/back/__tests__/models/JwtHandlerTest.php +++ /dev/null @@ -1,14 +0,0 @@ - 'bar']; - $token = $jwtHandler->jwtEncodeData('poolnet', $data); - $this->assertIsString($token); - } -} diff --git a/back/composer.json b/back/composer.json deleted file mode 100644 index ee123c5..0000000 --- a/back/composer.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "require": { - "firebase/php-jwt": "^6.5" - }, - "autoload": { - "psr-4": { - "PoolNET\\": "models/", - "PoolNET\\config\\": "config/", - "PoolNET\\MW\\": "middlewares/", - "PoolNET\\controller\\": "controllers/" - } - }, - "require-dev": { - "phpunit/phpunit": "^10.2" - } -} diff --git a/back/config/Database.php b/back/config/Database.php deleted file mode 100644 index 41648e1..0000000 --- a/back/config/Database.php +++ /dev/null @@ -1,43 +0,0 @@ -host = (string) getenv('ENV_DB_HOST'); - $this->dbName = (string) getenv('ENV_DB_NAME'); - $this->user = (string) getenv('ENV_DB_USER'); - $this->password = (string) getenv('ENV_DB_PSWD'); - } - /** - * Connecta a la base de dades - * @return PDO La connexió a la base de dades - */ - public function connect(): PDO - { - try { - $this->dbcnx = new PDO( - 'mysql:host=' . $this->host . ';dbname=' . $this->dbName, - $this->user, - $this->password, - [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]// Això per poder capturar errors diria. - ); - } catch (PDOException $err) { - echo 'Database connection failed: ' . $err->getMessage(); - return null; - } - return $this->dbcnx; - } -} diff --git a/back/controllers/Controlador.php b/back/controllers/Controlador.php deleted file mode 100644 index ddf2b99..0000000 --- a/back/controllers/Controlador.php +++ /dev/null @@ -1,62 +0,0 @@ -connect(); - } - /** - * Aplica les capceleres per a les respostes de l'API. - * @param string|null $allowMethod Mètodes permesos. GET per defecte. - */ - protected static function headers(?string $allowMethod = "GET"): void - { - Env::executar(); - header('Access-Control-Allow-Origin: ' . (string) getenv('ENV_HEADERS_ALLOW_ORIGIN')); - header('Access-Control-Allow-Methods: ' . $allowMethod); - header('Access-Control-Allow-Headers: ' . (string) getenv('ENV_HEADERS_ALLOW_HEADERS')); - header('Content-Type: application/json'); - } - /** - * Crea una resposta a la petició. - * @param int $status Codi d'estat http de la petició. - * @param array|null $response Resposta. - * @param bool $headers Si cal aplicar o no capceleres. - * @return void - */ - public static function respostaSimple(int $status = 500, ? array $response = null, bool $headers = true) : void - { - switch ($status) { - case 405: - if ($response === null) { - $response = ["error" => "Mètode no permès"]; - } - break; - default: - if ($response === null) { - $response = ["error" => "Alguna cosa ha fallat"]; - } - break; - } - if ($headers) { - self::headers('*'); - } - - http_response_code($status); - echo json_encode($response); - exit; - } -} diff --git a/back/controllers/control.php b/back/controllers/control.php deleted file mode 100644 index 3dfffbb..0000000 --- a/back/controllers/control.php +++ /dev/null @@ -1,104 +0,0 @@ - ['data_hora', 'DESC']], 20); - $num = count($result); - $num > 0 ? $res = $result : $res = ['message' => 'No s\'ha trobat cap control']; - parent::respostaSimple(200, $res, false); - } catch (\Throwable $th) { - parent::respostaSimple(500, ["error" => $th->getMessage()], false); - } - } - /** - * @param array $body El cos de la petició - * @return void - */ - public static function post(array $body): void - { - parent::headers("POST"); - $userData = json_decode(getenv('JWT_USER_DATA')); - try { - $control = new PoolNETControl($body); - $control->usuari = (int) $userData->userID; - if ($control->allNull()) { - parent::respostaSimple(400, ["error" => "Mínim has d'omplir un camp."], false); - } - if ($control->desar()) { - parent::respostaSimple(204, null, false); - } else { - parent::respostaSimple(500, ["error" => "No s'ha pogut desar el control de l'aigua."], false); - } - } catch (\Throwable $th) { - parent::respostaSimple(400, ["error" => $th->getMessage()], false); - } - } - /** - * @param array $body El cos de la petició - * @return void - */ - public static function patch(array $body): void - { - parent::headers("PATCH"); - try { - $userData = json_decode(getenv('JWT_USER_DATA')); - $controlAEditar = PoolNETControl::trobarPerUnic('controlID', (int) $body['controlID']); - if ($controlAEditar === null) { - parent::respostaSimple(404, ["error" => "No s'ha trobat el control."], false); - } - $controlAEditar->getDadesUsuari(); - if ($controlAEditar->user->userID != (int) $userData->userID && (int) $userData->nivell > 0) { - parent::respostaSimple(403, ["error" => "Només pots editar controls propis."], false); - } - foreach ($body as $camp => $valor) { - $controlAEditar->$camp = $valor; - } - if ($controlAEditar->allNull()) { - parent::respostaSimple(400, ["error" => "No pots buidar un control."], false); - } - if ($controlAEditar->desar()) { - parent::respostaSimple(204, null, false); - } else { - parent::respostaSimple(500, ["error" => "No s'ha pogut desar el control."], false); - } - } catch (\Throwable $th) { - parent::respostaSimple(400, ["error" => $th->getMessage()], false); - } - } - /** - * @param array $body El cos de la petició - * @return void - */ - public static function delete(array $body): void - { - parent::headers("DELETE"); - try { - $userData = json_decode(getenv('JWT_USER_DATA')); - $controlAEliminar = PoolNETControl::trobarPerUnic('controlID', (int) $body['controlID']); - if ($controlAEliminar === null) { - parent::respostaSimple(404, ["error" => "No s'ha trobat el control."], false); - } - $controlAEliminar->getDadesUsuari(); - if ($controlAEliminar->user->userID != (int) $userData->userID && (int) $userData->nivell > 0) { - parent::respostaSimple(403, ["error" => "Només pots eliminar controls propis."], false); - } - if ($controlAEliminar->borrar()) { - parent::respostaSimple(204, null, false); - } else { - parent::respostaSimple(500, ["error" => "No s'ha pogut borrar el control."], false); - } - } catch (\Throwable $th) { - parent::respostaSimple(400, ["error" => $th->getMessage()], false); - } - } -} diff --git a/back/middlewares/AuthMW.php b/back/middlewares/AuthMW.php deleted file mode 100644 index 226684d..0000000 --- a/back/middlewares/AuthMW.php +++ /dev/null @@ -1,61 +0,0 @@ -jwtDecodeData($_COOKIE['token']); - if (!isset($data->userID)) { - return false; - } - $user = User::trobarPerId((int) $data->userID); - if ($user === null) { - return false; - } - return true; - - } - /** - * Comprova el token rebut a les cookies de la petició i permet seguir si aquest és vàlid. Si no, atura la petició amb un 401. - * @return void Les dades de l'usuari són afegides a la variable d'entorn JWT_USER_DATA - */ - public static function rutaProtegida(): void - { - if (parent::$dbcnx === null) { - parent::connect(); - } - - if (!self::isValid()) { - self::respostaSimple(401, ["error" => "No autoritzat"], true); - } - if (self::$jwt === null) { - self::initJwtHandler(); - } - $userData = self::$jwt->jwtDecodeData($_COOKIE['token']); - putenv('JWT_USER_DATA=' . json_encode($userData)); - } -} diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..63d2951 --- /dev/null +++ b/css/style.css @@ -0,0 +1,252 @@ +body { + background: #00ffff; + background-image: url("../img/fons_aigua.jpg"); + background-size: 100%; + margin-right: auto; + margin-left: auto; + font-size: 22px; + max-width: 1000px; + text-align: center; +} + +h1, +h2, +h3 { + text-align: center; +} + +h1.principal { + text-shadow: 2px 0 #0ff, -2px 0 #0ff, 0 2px #0ff, 0 -2px #0ff, 1px 1px #0ff, + -1px -1px #0ff, 1px -1px #0ff, -1px 1px #0ff; +} + +div.head_user { + position: sticky; + width: 100%; + text-align: right; + letter-spacing: 0.7em; + border: 1px solid black; + border-radius: 10px; + background-color: #00ffff50; + padding-top: 10px; + padding-bottom: 10px; + top: 10px; + z-index: 10; +} +span.head_user { + margin-right: 10px; + margin-bottom: auto; +} + +span#inici { + float: left; + margin-left: 10px; +} + +div.boto_inici { + border: 2px solid black; + border-radius: 30px; + width: 100%; + margin-top: 20px; + margin-bottom: 20px; +} + +div#estat { + background-color: rgb(255, 0, 0, 0.8); +} + +div#control { + background-color: rgb(0, 128, 0, 0.8); +} + +div#accio { + background-color: rgb(0, 0, 255, 0.8); +} + +div.form { + border: 1px solid black; + border-radius: 5px; + background-color: #00ffff80; +} + +/* CONTROL */ +div.control { + border: 2px solid black; + border-radius: 30px; + width: 95%; + padding-top: 100%; /* 1:1 Aspect Ratio */ + position: relative; + background-color: #00ffff80; + cursor: pointer; +} +div.controlText { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + height: 100%; + text-align: center; + display: flex; + align-items: center; + justify-content: center; +} +.controlText h1, +h2 { + text-shadow: 2px 0 #fff, -2px 0 #fff, 0 2px #fff, 0 -2px #fff, 1px 1px #fff, + -1px -1px #fff, 1px -1px #fff, -1px 1px #fff; +} +div#total { + padding: 5px; + height: 50px; + border-radius: 15px; + background-image: url(img/caseta.jpg); + background-size: 100%; + background-position: center; +} + +div#phclor { + background-image: url(img/phclor.jpg); + background-size: 100%; +} + +div#aigua { + background-image: url(img/aigua.jpg); + background-size: 100%; +} + +div#phmes { + background-image: url(img/phmes.jpg); + background-size: 66%; + background-position: center; + background-repeat: no-repeat; +} + +div#phmenys { + background-image: url(img/phmenys.jpg); + background-size: 66%; + background-position: center; + background-repeat: no-repeat; +} +div#clor { + background-image: url(img/clor.jpg); + background-size: 66%; + background-position: center; + background-repeat: no-repeat; +} +div#antialga { + background-image: url(img/antialga.jpg); + background-size: 66%; + background-position: center; + background-repeat: no-repeat; +} +div#alcali { + background-image: url(img/alka.jpg); + background-size: 66%; + background-position: center; + background-repeat: no-repeat; +} +div#fluoculant { + background-image: url(img/fluoculant.jpg); + background-size: 66%; + background-position: center; + background-repeat: no-repeat; +} + +div#aglutinant { + background-image: url(img/aglutinant.jpg); + background-size: 66%; + background-position: center; + background-repeat: no-repeat; +} + +table.control { + border: none; + border-collapse: collapse; + width: 100%; + text-align: center; + margin-bottom: 10px; +} + +table#accio { + background-image: url(img/prestatge.jpg); + background-size: 100%; + border-radius: 20px; +} + +table.formTable { + border: none; + border-collapse: collapse; + width: 100%; + text-align: center; + margin-left: auto; + margin-right: auto; + margin-bottom: 10px; +} + +table.centre { + border: 1px solid black; + border-collapse: collapse; + text-align: center; + margin-left: auto; + margin-right: auto; +} + +div.avisTemporal { + border: 3px solid red; + border-radius: 10px; + background-color: #00ffff80; + padding-left: 10px; + padding-right: 10px; + padding-top: 0px; + padding-bottom: 5px; + margin-bottom: 20px; + position: relative; +} + +div.alertaUrgent { + border: 2px solid black; + border-radius: 10px; + background-color: #ff8000bb; + padding-left: 10px; + padding-right: 10px; + padding-top: 0px; + padding-bottom: 5px; + margin-bottom: 20px; + text-align: left; + position: relative; +} +.alertaUrgent h2 { + text-shadow: 2px 0 #f00, -2px 0 #f00, 0 2px #f00, 0 -2px #f00, 1px 1px #f00, + -1px -1px #f00, 1px -1px #f00, -1px 1px #f00; +} + +div.tancar { + position: absolute; + top: 0; + right: 0; + margin: 10px; + z-index: 1; + cursor: pointer; +} + +.avisTemporal h2 { + margin-bottom: -10px; + text-shadow: 2px 0 #f00, -2px 0 #f00, 0 2px #f00, 0 -2px #f00, 1px 1px #f00, + -1px -1px #f00, 1px -1px #f00, -1px 1px #f00; +} + +a.dissimulat:link { + text-decoration: none; + color: black; +} + +a.dissimulat:visited { + text-decoration: none; + color: black; +} + +a.dissimulat:hover { + text-decoration: underline; + color: red; +} diff --git a/img/fons_aigua.jpg b/img/fons_aigua.jpg new file mode 100644 index 0000000..01ba4ae Binary files /dev/null and b/img/fons_aigua.jpg differ diff --git a/index.php b/index.php new file mode 100644 index 0000000..2146f77 --- /dev/null +++ b/index.php @@ -0,0 +1,20 @@ +addRouter("/api", apiRouter()); +$router->addRouter("/", pageRouter()); + +$router->use($req, $res); diff --git a/sonar-project.properties b/sonar-project.properties index 16b9e44..9c44c67 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,14 +2,14 @@ sonar.projectKey=oriolsastre_PoolNET sonar.organization=oriolsastre sonar.projectBaseDir=. -sonar.sources=back/ -sonar.tests=back/__tests__/ +sonar.sources=src/ +sonar.tests=src/__tests__/ -sonar.exclusions=back/__tests__/**/*,back/vendor/**/* -sonar.tests.exclusions=back/__tests_/E2E/**/* +sonar.exclusions=src/__tests__/**/*,src/vendor/**/*,src/config/database.sql +sonar.tests.exclusions=src/__tests_/E2E/**/* -sonar.php.tests.reportPath=back/execution.xml -sonar.php.coverage.reportPaths=back/coverage.xml +sonar.php.tests.reportPath=src/execution.xml +sonar.php.coverage.reportPaths=src/coverage.xml # This is the name and version displayed in the SonarCloud UI. #sonar.projectName=PoolNET diff --git a/back/README.md b/src/README.md similarity index 85% rename from back/README.md rename to src/README.md index 09e0470..0ca72c3 100644 --- a/back/README.md +++ b/src/README.md @@ -1,12 +1,24 @@ API senzilla. +## Install + +Per a instal·lar dependències: + +`composer update` + +Per a acutalitzar l'autoload: + +`composer dump-autoload` + +Fer còpies de `env-example.php` i `.htaccess-example`. + ## Tests Fer anar els tests amb `./vendor/bin/phpunit --testdox __tests__` -## Docs +# Docs Hi ha 3 endpoints, 1 de login i els altres dos per a funcionalitats CRUD d'accions i controls sobre l'aigua. diff --git a/back/__tests__/E2E/auth/login.test.ts b/src/__tests__/E2E/auth/login.test.ts similarity index 100% rename from back/__tests__/E2E/auth/login.test.ts rename to src/__tests__/E2E/auth/login.test.ts diff --git a/back/__tests__/E2E/constants.example.ts b/src/__tests__/E2E/constants.example.ts similarity index 100% rename from back/__tests__/E2E/constants.example.ts rename to src/__tests__/E2E/constants.example.ts diff --git a/back/__tests__/E2E/control/get.test.ts b/src/__tests__/E2E/control/get.test.ts similarity index 83% rename from back/__tests__/E2E/control/get.test.ts rename to src/__tests__/E2E/control/get.test.ts index 6a6ea54..52380fa 100644 --- a/back/__tests__/E2E/control/get.test.ts +++ b/src/__tests__/E2E/control/get.test.ts @@ -9,13 +9,13 @@ function isNullOrNumber(value: any) { describe("Testejant l'endpoint GET de control", () => { it("Hauria de rebre un array amb els últims controls", async () => { - const response = await req.get('/'); + const response = await req.get("/"); expect(response.status).toBe(200); expect(response.body).toBeInstanceOf(Array); - expect(response.body.length).toBeLessThanOrEqual(20) + expect(response.body.length).toBeLessThanOrEqual(20); response.body.forEach((control: any) => { - expect(typeof control.controlID).toBe("number"); + expect(typeof control.controlId).toBe("number"); expect(typeof control.data_hora).toBe("string"); expect(isNullOrNumber(control.ph)).toBe(true); expect(isNullOrNumber(control.clor)).toBe(true); @@ -25,9 +25,9 @@ describe("Testejant l'endpoint GET de control", () => { expect(isNullOrNumber(control.fons)).toBe(true); expect(typeof control.usuari).toBe("number"); expect(typeof control.user).toBe("object"); - expect(typeof control.user.userID).toBe("number"); + expect(typeof control.user.usuariId).toBe("number"); expect(typeof control.user.usuari).toBe("string"); expect(typeof control.user.nivell).toBe("number"); - }) + }); }); }); diff --git a/back/__tests__/E2E/control/post_update_delete.test.ts b/src/__tests__/E2E/control/post_update_delete.test.ts similarity index 94% rename from back/__tests__/E2E/control/post_update_delete.test.ts rename to src/__tests__/E2E/control/post_update_delete.test.ts index e474468..c60fc30 100644 --- a/back/__tests__/E2E/control/post_update_delete.test.ts +++ b/src/__tests__/E2E/control/post_update_delete.test.ts @@ -96,7 +96,7 @@ describe("Testejant l'endpoint PATCH de control", () => { }); it("Hauria de fallar amb valor invàlid", async () => { const response = await req.patch("/").set("Cookie", cookie).send({ - controlID: ultimControl[1].controlID, + controlId: ultimControl[1].controlId, ph: 7.0, transparent: "Molt", }); @@ -105,7 +105,7 @@ describe("Testejant l'endpoint PATCH de control", () => { }); it("Hauria de fallar amb tot null", async () => { const response = await req.patch("/").set("Cookie", cookie).send({ - controlID: ultimControl[1].controlID, + controlId: ultimControl[1].controlId, ph: null, clor: null, alcali: null, @@ -118,7 +118,7 @@ describe("Testejant l'endpoint PATCH de control", () => { }); it("Hauria de fallar si no troba el control", async () => { const response = await req.patch("/").set("Cookie", cookie).send({ - controlID: -5, + controlId: -5, ph: 7.2, clor: 0.1, alcali: 1.1, @@ -132,7 +132,7 @@ describe("Testejant l'endpoint PATCH de control", () => { describe("Problemes de permisos", () => { it("Hauria de fallar si intenta modificar un control aliè", async () => { const response = await req.patch("/").set("Cookie", cookie).send({ - controlID: ultimControl[0].controlID, + controlId: ultimControl[0].controlId, ph: 8.2, clor: 0.1, alcali: null, @@ -151,7 +151,7 @@ describe("Testejant l'endpoint PATCH de control", () => { describe("Testejant l'èxit", () => { it("Hauria de modificar un control propi", async () => { const body = { - controlID: ultimControl[1].controlID, + controlId: ultimControl[1].controlId, ph: 8, clor: 0.1, alcali: null, @@ -166,7 +166,7 @@ describe("Testejant l'endpoint PATCH de control", () => { }); it("L'admin hauria de poder editar un control aliè", async () => { const body = { - controlID: ultimControl[1].controlID, + controlId: ultimControl[1].controlId, ph: 4, clor: 3.5, fons: null, @@ -208,7 +208,7 @@ describe("Testejant l'enpoint DELETE de control", () => { it("Hauria de fallar amb body invàlid (valor incorrecte)", async () => { const response = await req .delete("/") - .send({ controlID: "dos" }) + .send({ controlId: "dos" }) .set("Cookie", cookie); expect(response.status).toBe(400); expect(response.body.error).toBe( @@ -219,7 +219,7 @@ describe("Testejant l'enpoint DELETE de control", () => { it("Hauria de fallar si no troba el control", async () => { const response = await req .delete("/") - .send({ controlID: -5 }) + .send({ controlId: -5 }) .set("Cookie", cookie); expect(response.status).toBe(404); expect(response.body.error).toBe("No s'ha trobat el control."); @@ -227,7 +227,7 @@ describe("Testejant l'enpoint DELETE de control", () => { it("Hauria de fallar si intenta eliminar un control aliè", async () => { const response = await req .delete("/") - .send({ controlID: ultimControl[0].controlID }) + .send({ controlId: ultimControl[0].controlId }) .set("Cookie", cookie); expect(response.status).toBe(403); expect(response.body.error).toBe( @@ -240,14 +240,14 @@ describe("Testejant l'enpoint DELETE de control", () => { it("Hauria de poder eliminar un control propi", async () => { const response = await req .delete("/") - .send({ controlID: ultimControl.pop().controlID }) + .send({ controlId: ultimControl.pop().controlId }) .set("Cookie", cookie); expect(response.status).toBe(204); }); it("L'admin hauria de poder eliminar un control aliè", async () => { const response = await req .delete("/") - .send({ controlID: ultimControl.pop().controlID }) + .send({ controlId: ultimControl.pop().controlId }) .set("Cookie", cookieAdmin); expect(response.status).toBe(204); }); diff --git a/back/__tests__/E2E/jest.config.js b/src/__tests__/E2E/jest.config.js similarity index 100% rename from back/__tests__/E2E/jest.config.js rename to src/__tests__/E2E/jest.config.js diff --git a/back/__tests__/E2E/package-lock.json b/src/__tests__/E2E/package-lock.json similarity index 99% rename from back/__tests__/E2E/package-lock.json rename to src/__tests__/E2E/package-lock.json index 0b2632c..ad4fb19 100644 --- a/back/__tests__/E2E/package-lock.json +++ b/src/__tests__/E2E/package-lock.json @@ -1,11 +1,11 @@ { - "name": "poolnet-back", + "name": "poolnet", "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "poolnet-back", + "name": "poolnet", "version": "1.0.0", "license": "ISC", "devDependencies": { diff --git a/back/__tests__/E2E/package.json b/src/__tests__/E2E/package.json similarity index 94% rename from back/__tests__/E2E/package.json rename to src/__tests__/E2E/package.json index 6764e8e..203bec1 100644 --- a/back/__tests__/E2E/package.json +++ b/src/__tests__/E2E/package.json @@ -1,5 +1,5 @@ { - "name": "poolnet-back", + "name": "poolnet", "version": "1.0.0", "description": "Backend de PoolNET. Tests.", "main": "index.js", diff --git a/back/__tests__/E2E/setup.ts b/src/__tests__/E2E/setup.ts similarity index 100% rename from back/__tests__/E2E/setup.ts rename to src/__tests__/E2E/setup.ts diff --git a/back/__tests__/E2E/tsconfig.json b/src/__tests__/E2E/tsconfig.json similarity index 100% rename from back/__tests__/E2E/tsconfig.json rename to src/__tests__/E2E/tsconfig.json diff --git a/src/__tests__/ReqResTestCase.php b/src/__tests__/ReqResTestCase.php new file mode 100644 index 0000000..6b4057c --- /dev/null +++ b/src/__tests__/ReqResTestCase.php @@ -0,0 +1,30 @@ +req = new Request(); + $this->res = new Response(); + } + protected function getAssocHeaders(): array + { + $headers = xdebug_get_headers(); + $assocHeaders = array(); + foreach ($headers as $header) { + $header = explode(':', $header); + $assocHeaders[$header[0]] = $header[1]; + } + return $assocHeaders; + } +} diff --git a/src/__tests__/config/DatabaseTest.php b/src/__tests__/config/DatabaseTest.php new file mode 100644 index 0000000..d878e9c --- /dev/null +++ b/src/__tests__/config/DatabaseTest.php @@ -0,0 +1,63 @@ +assertInstanceOf(Database::class, $database); + $reflectedDB = new ReflectionObject($database); + $reflectedDB->getProperty('dbName')->setAccessible(true); + $this->assertSame(getenv('ENV_DB_NAME'), $reflectedDB->getProperty('dbName')->getValue($database)); + } + /** + * @coversNothing + * @doesNotPerformAssertions + */ + private function getObjectProtectedProperty(string $property) + { + $reflectionClass = new ReflectionClass('PoolNET\config\Database'); + $reflectionProperty = $reflectionClass->getProperty($property); + $reflectionProperty->setAccessible(true); + return $reflectionProperty->getValue((object) $reflectionClass->newInstance()); + } + /** + * @covers \PoolNET\config\database\Database::connect + * @uses \PoolNET\config\Env + */ + public function testConnect(): void + { + // Testejant l'èxit + $database = new Database(); + $dbcnx = $database->connect(); + $this->assertInstanceOf(PDO::class, $dbcnx); + + // Testejant l'error + // TODO: Testejar el throw + // $reflectedDB = new ReflectionClass('PoolNET\config\Database'); + // $instance = (object) $reflectedDB->newInstance(); + // $reflectedDB->getProperty('dbName')->setValue($instance, 'invalidHost'); + // $dbcnx2 = $instance->connect(); + // $this->expectOutputRegex('/^Database connection failed:/'); + // $this->assertNull($dbcnx2); + + } +} diff --git a/src/__tests__/config/EnvTest.php b/src/__tests__/config/EnvTest.php new file mode 100644 index 0000000..d6412e8 --- /dev/null +++ b/src/__tests__/config/EnvTest.php @@ -0,0 +1,22 @@ +assertNotEmpty(getenv('ENV_DB_NAME')); + $this->assertNotEmpty(getenv('ENV_HEADERS_ALLOW_ORIGIN')); + $this->assertNotEmpty(getenv('ENV_HEADERS_ALLOW_HEADERS')); + $this->assertNotEmpty(getenv('ENV_JWTSecret')); + $this->assertNotEmpty(getenv('ENV_ServerSalt')); + } +} diff --git a/src/__tests__/config/RequestTest.php b/src/__tests__/config/RequestTest.php new file mode 100644 index 0000000..bb29539 --- /dev/null +++ b/src/__tests__/config/RequestTest.php @@ -0,0 +1,130 @@ +assertSame($this->REQUEST_URI, $request->getUri()); + } + /** + * @covers \PoolNET\config\Request + * @covers ::getPath + */ + public function testGetPath(): void + { + $request = new Request(); + $this->assertSame("/tests", $request->getPath()); + } + /** + * @covers \PoolNET\config\Request + * @covers ::withPath + */ + public function testWithPath(): void + { + $request = new Request(); + $newPath = "/custom"; + $this->assertSame($newPath, $request->withPath($newPath)->getPath()); + } + /** + * @covers \PoolNET\config\Request + * @covers ::getParams + */ + public function testGetParams(): void + { + $request = new Request(); + $this->assertSame('', $request->getParams()); + + $originalUri = $this->REQUEST_URI; + $_SERVER["REQUEST_URI"] = $this->REQUEST_URI . '?param1=value1¶m2=value2'; + $request2 = new Request(); + $this->assertSame('param1=value1¶m2=value2', $request2->getParams()); + $_SERVER["REQUEST_URI"] = $originalUri; + } + /** + * @covers \PoolNET\config\Request + * @covers ::getMethod + */ + public function testGetMethod(): void + { + $request = new Request(); + $this->assertSame("get", $request->getMethod()); + } + /** + * @covers \PoolNET\config\Request + * @covers ::withMethod + */ + public function testWithMethod(): void + { + $request = new Request(); + $request = $request->withMethod("post"); + $this->assertSame("post", $request->getMethod()); + } + /** + * @covers \PoolNET\config\Request + * @covers ::getHeaders + */ + public function testGetHeaders(): void + { + $request = new Request(); + + // Simulate a request with header + $_SERVER['HTTP_ACCEPT'] = "application/json"; + $this->assertArrayHasKey("Accept", $request->getHeaders()); + $this->assertSame("application/json", $request->getHeaders()["Accept"]); + } + /** + * @covers \PoolNET\config\Request + * @covers ::getParsedBody + */ + public function testGetParsedBody(): void + { + $request = new Request(); + $this->assertSame([], $request->getParsedBody()); + // TODO: Testejar amb un body amb dades. Potser mockejant. + } + /** + * @covers \PoolNET\config\Request + * @covers ::withBody + */ + public function testWithBody(): void + { + $request = new Request(); + $request = $request->withBody(["key" => "value"]); + $this->assertSame(["key" => "value"], $request->getParsedBody()); + } + /** + * @covers \PoolNET\config\Request + * @covers ::getCookieParams + */ + public function testGetCookieParams(): void + { + $request = new Request(); + $this->assertSame($_COOKIE, $request->getCookieParams()); + } + /** + * @covers \PoolNET\config\Request + * @covers ::withCookieParams + */ + public function testWithCookieParams(): void + { + $request = new Request(); + $request = $request->withCookieParams(["key" => "value"]); + $this->assertArrayHasKey("key", $request->getCookieParams()); + $this->assertSame("value", $request->getCookieParams()["key"]); + } +} diff --git a/src/__tests__/config/ResponseTest.php b/src/__tests__/config/ResponseTest.php new file mode 100644 index 0000000..7245bf0 --- /dev/null +++ b/src/__tests__/config/ResponseTest.php @@ -0,0 +1,144 @@ + "Content-Type", "value" => 'application/json']; + private array $header2 = ["key" => "Accept", "value" => ['application/json', 'text/html']]; + + public function setUp(): void + { + $this->getResponsePrivates(); + } + /** + * @covers ::withHeader + */ + public function testWithHeader(): void + { + $response = new Response(); + // Check initial values + $this->assertInstanceOf(Response::class, $response); + $this->assertSame([], $this->headers->getValue($response)); + $this->assertEquals(200, $this->status->getValue($response)); + + // Using string + $response->withHeader($this->header1["key"], $this->header1["value"]); + $this->assertSame($this->header1["value"], $this->headers->getValue($response)[$this->header1["key"]]); + // Using array as header value + $response->withHeader($this->header2["key"], $this->header2["value"]); + $this->assertSame(implode(",", $this->header2["value"]), $this->headers->getValue($response)[$this->header2["key"]]); + } + /** + * @covers ::sendHeaders + * @uses \PoolNET\config\Response + */ + public function testSendHeaders(): void + { + ob_start(); + $response = new Response(); + $response->withHeader($this->header1["key"], $this->header1["value"]); + $this->sendHeaders->invoke($response); + + $this->assertTrue(in_array('Content-Type: application/json', xdebug_get_headers())); + ob_end_clean(); + } + /** + * @covers ::withStatus + * @uses \PoolNET\config\Response + */ + public function testWithStatus(): void + { + $response = new Response(); + // Default is 200 + $this->assertEquals(200, $this->status->getValue($response)); + // Custom status + $result = $response->withStatus(404); + $this->assertEquals(404, $this->status->getValue($response)); + $this->assertSame($response, $result); + } + /** + * @covers ::toJson + * @covers ::defaultResponse + * @uses \PoolNET\config\Response + */ + public function testToJson(): void + { + $body = ["data" => "value"]; + $response = new Response(); + + ob_start(); + // Normal case + $response->toJson($body); + $this->assertTrue(in_array('Content-Type: application/json', xdebug_get_headers())); + $this->assertEquals(200, http_response_code()); + $this->assertSame(json_encode($body), ob_get_contents()); + ob_end_clean(); + + // Default response 405 + ob_start(); + $response->withStatus(405)->toJson(null); + $this->assertTrue(in_array('Content-Type: application/json', xdebug_get_headers())); + $this->assertEquals(405, http_response_code()); + $this->assertSame(json_encode(["error" => "Mètode no permès"]), ob_get_contents()); + ob_end_clean(); + + // Default response 500 + ob_start(); + $response->withStatus(500)->toJson(null); + $this->assertTrue(in_array('Content-Type: application/json', xdebug_get_headers())); + $this->assertEquals(500, http_response_code()); + $this->assertSame(json_encode(["error" => "Alguna cosa ha fallat"]), ob_get_contents()); + ob_end_clean(); + } + /** + * @covers ::handleError + * @uses \PoolNET\config\Response + */ + public function testHandleError(): void + { + $response = new Response(); + + // Database error + ob_start(); + $response->handleError(new PDOException()); + $this->assertTrue(in_array('Content-Type: application/json', xdebug_get_headers())); + $this->assertEquals(500, http_response_code()); + $this->assertSame(json_encode(["error" => "Error amb la base de dades"]), ob_get_contents()); + ob_end_clean(); + + // Generic error + ob_start(); + $response->handleError(new Exception()); + $this->assertTrue(in_array('Content-Type: application/json', xdebug_get_headers())); + $this->assertEquals(500, http_response_code()); + $this->assertSame(json_encode(["error" => "Alguna cosa ha fallat"]), ob_get_contents()); + ob_end_clean(); + } + /** + * @coversNothing + * @doesNotPerformAssertions + */ + private function getResponsePrivates(): void + { + $reflectionClass = new ReflectionClass(Response::class); + $this->headers = $reflectionClass->getProperty('headers'); + $this->headers->setAccessible(true); + $this->status = $reflectionClass->getProperty('status'); + $this->status->setAccessible(true); + $this->sendHeaders = $reflectionClass->getMethod('sendHeaders'); + $this->sendHeaders->setAccessible(true); + $this->defaultResponse = $reflectionClass->getMethod('defaultResponse'); + $this->defaultResponse->setAccessible(true); + } +} diff --git a/src/__tests__/controllers/AuthLoginTest.php b/src/__tests__/controllers/AuthLoginTest.php new file mode 100644 index 0000000..638ef16 --- /dev/null +++ b/src/__tests__/controllers/AuthLoginTest.php @@ -0,0 +1,50 @@ +newReqRes(); + $authLoginController = new AuthLogin(); + + // Test exit + $this->req = $this->req->withBody(["usuari" => $this->testUser, "password" => $this->testPswd]); + ob_start(); + $authLoginController->post($this->req, $this->res); + $this->assertEquals(302, http_response_code()); + $headers = $this->getAssocHeaders(); + $this->assertArrayHasKey('Set-Cookie', $headers); + $this->assertStringContainsString('token=', $headers['Set-Cookie']); + ob_end_clean(); + + // Test error + $this->newReqRes(); + $this->req = $this->req->withBody(["usuari" => "Test", "password" => "Test123"]); + ob_start(); + $authLoginController->post($this->req, $this->res); + $this->assertEquals(400, http_response_code()); + + ob_end_clean(); + } +} diff --git a/src/__tests__/controllers/ControlTest.php b/src/__tests__/controllers/ControlTest.php new file mode 100644 index 0000000..c7d50f4 --- /dev/null +++ b/src/__tests__/controllers/ControlTest.php @@ -0,0 +1,38 @@ +newReqRes(); + $controlController = new Control(); + + // Test exit + ob_start(); + $controlController->get($this->req, $this->res); + $this->assertEquals(200, http_response_code()); + + $output = ob_get_contents(); + $outputJson = json_decode($output, true); + $this->assertIsArray($outputJson); + $this->assertArrayHasKey("controlId", $outputJson[0]); + + ob_end_clean(); + } +} diff --git a/src/__tests__/middlewares/LoginValidatorTest.php b/src/__tests__/middlewares/LoginValidatorTest.php new file mode 100644 index 0000000..b50fe05 --- /dev/null +++ b/src/__tests__/middlewares/LoginValidatorTest.php @@ -0,0 +1,50 @@ +newReqRes(); + $this->req = $this->req->withBody(["usuari" => "Test", "password" => "Test123"]); + + $validator = new LoginValidator(); + $result = $validator->use($this->req, $this->res); + $this->assertTrue($result); + + // Test falla per tipus invalid + $this->req = $this->req->withBody(["usuari" => "Test", "password" => 123]); + ob_start(); + $result = $validator->use($this->req, $this->res); + $this->assertFalse($result); + $this->assertEquals(400, http_response_code()); + $response = json_decode(ob_get_contents(), true); + $this->assertArrayHasKey("error", $response); + $this->assertSame("Algun camp no és del tipus correcte.", $response["error"]); + $this->assertArrayHasKey("camps_obligatoris", $response); + ob_end_clean(); + + // Test falla per falta de camp obligatori + $this->req = $this->req->withBody(["usuari" => "Test"]); + ob_start(); + $result = $validator->use($this->req, $this->res); + $this->assertFalse($result); + $this->assertEquals(400, http_response_code()); + $response = json_decode(ob_get_contents(), true); + $this->assertArrayHasKey("error", $response); + $this->assertSame("Falta algun camp obligatori.", $response["error"]); + $this->assertArrayHasKey("camps_obligatoris", $response); + ob_end_clean(); + } +} diff --git a/src/__tests__/models/ControlTest.php b/src/__tests__/models/ControlTest.php new file mode 100644 index 0000000..bd161cd --- /dev/null +++ b/src/__tests__/models/ControlTest.php @@ -0,0 +1,134 @@ +assertInstanceOf(Control::class, $control); + $reflectControl = new ReflectionObject($control); + $reflectControl->getProperty('table')->setAccessible(true); + $this->assertSame('control', $reflectControl->getProperty('table')->getValue($control)); + $reflectControl->getProperty('idKey')->setAccessible(true); + $this->assertSame('controlId', $reflectControl->getProperty('idKey')->getValue($control)); + $reflectControl->getProperty('uniqueKeyValues')->setAccessible(true); + $this->assertSame(['controlId'], $reflectControl->getProperty('uniqueKeyValues')->getValue($control)); + } + /** + * @covers \PoolNET\Control::__construct + * @covers \PoolNET\Model::__construct + * @uses \PoolNET\Model + */ + public function testConstructorWithDataNoUser(): void + { + $data = [ + 'data_hora' => date('Y-m-d H:i:s'), + 'ph' => 6.8, + 'clor' => 0.1, + 'temperatura' => 31, + 'transparent' => 1, + 'fons' => 1, + ]; + $control = new Control($data); + $this->assertInstanceOf(Control::class, $control); + $this->assertNull($control->controlId); + $this->assertSame($data['data_hora'], $control->data_hora); + $this->assertSame($data['ph'], $control->ph); + $this->assertSame($data['clor'], $control->clor); + $this->assertSame($data['temperatura'], $control->temperatura); + $this->assertSame($data['transparent'], $control->transparent); + $this->assertSame($data['fons'], $control->fons); + $this->assertNull($control->usuari); + $this->assertNull($control->user); + } + /** + * @covers \PoolNET\Control::__construct + * @covers \PoolNET\Model::__construct + * @uses \PoolNET\User + * @uses \PoolNET\Control::getDadesUsuari + */ + public function testConstructorWithDataAndUser(): void + { + $data = [ + 'data_hora' => date('Y-m-d H:i:s'), + 'ph' => 6.8, + 'clor' => 0.1, + 'temperatura' => 31, + 'transparent' => 1, + 'fons' => 1, + 'usuari' => 1, + ]; + $control = new Control($data); + $this->assertInstanceOf(Control::class, $control); + $this->assertNull($control->controlId); + $this->assertSame($data['data_hora'], $control->data_hora); + $this->assertSame($data['ph'], $control->ph); + $this->assertSame($data['clor'], $control->clor); + $this->assertSame($data['temperatura'], $control->temperatura); + $this->assertSame($data['transparent'], $control->transparent); + $this->assertSame($data['fons'], $control->fons); + $this->assertSame($data['usuari'], $control->usuari); + $this->assertInstanceOf('PoolNET\User', $control->user); + $this->assertSame($data['usuari'], $control->user->usuariId); + } + /** + * @covers \PoolNET\Control::getDadesUsuari + * @uses \PoolNET\User + */ + public function testGetDadesUsuari(): void + { + $control = new Control(); + $this->assertNull($control->usuari); + $this->assertNull($control->user); // És null abans + $this->assertFalse($control->getDadesUsuari()); + $this->assertNull($control->user); // És null després + + $control->usuari = 1; + $this->assertTrue($control->getDadesUsuari()); + $this->assertInstanceOf('PoolNET\User', $control->user); + $this->assertSame(1, $control->user->usuariId); + + $control->usuari = 90; //No hauria d'existir + $this->assertFalse($control->getDadesUsuari()); + } + /** + * @covers \PoolNET\Control::allNull + */ + public function testAllNull(): void + { + $control = new Control(); + $this->assertTrue($control->allNull()); + + $control->controlId = 1; + $control->usuari = 1; + $control->getDadesUsuari(); + $this->assertNotNull($control->controlId); + $this->assertNotNull($control->usuari); + $this->assertNotNull($control->user); + $this->assertNotNull($control->data_hora); + $this->assertTrue($control->allNull()); + + $control->ph = 7; + $this->assertFalse($control->allNull()); + } +} diff --git a/src/__tests__/models/ModelTest.php b/src/__tests__/models/ModelTest.php new file mode 100644 index 0000000..3aaa3f3 --- /dev/null +++ b/src/__tests__/models/ModelTest.php @@ -0,0 +1,70 @@ +mockedExtension = new class extends Model + { + protected static string $table = "mockedTable"; + protected static string $idKey = "mockId"; + protected static array $uniqueKeyValues = ["mockId"]; + public ?int $mockId = null; + public ?string $value1 = null; + public string $value2; + }; + } + /** + * @covers \PoolNET\Model::__construct + */ + public function testConstructorWithNoData(): void + { + $model = $this->getMockBuilder(Model::class)->getMock(); + $this->assertInstanceOf(Model::class, $model); + // $this->assertNull($model->dbcnx); + } + /** + * @covers \PoolNET\Model::__construct + */ + public function testConstructorWithData(): void + { + $mockedModel = new $this->mockedExtension(["value1" => "value1", "value2" => "value2"]); + $this->assertInstanceOf(Model::class, $mockedModel); + $this->assertSame("value1", $mockedModel->value1); + $this->assertSame("value2", $mockedModel->value2); + $this->assertNull($mockedModel->mockId); + } + /** + * @covers \PoolNET\Model::connect + * @uses \PoolNET\config\database\Database + */ + public function testConnect(): void + { + $mockedModel = new $this->mockedExtension(); + $this->assertInstanceOf(Model::class, $mockedModel); + $reflectedModel = new ReflectionClass(Model::class); + + $dbcnxProp = $reflectedModel->getProperty('dbcnx'); + $dbcnxProp->setAccessible(true); + + $connectMethod = $reflectedModel->getMethod('connect'); + $connectMethod->setAccessible(true); + $connectMethod->invoke($mockedModel); + // $dbcnx is instance of PDO after connecting + $this->assertNotNull($dbcnxProp->getValue()); + $this->assertInstanceOf(PDO::class, $dbcnxProp->getValue()); + } +} diff --git a/src/__tests__/models/UserTest.php b/src/__tests__/models/UserTest.php new file mode 100644 index 0000000..ae249dd --- /dev/null +++ b/src/__tests__/models/UserTest.php @@ -0,0 +1,67 @@ +assertInstanceOf(User::class, $user); + $reflectUser = new ReflectionObject($user); + $reflectUser->getProperty('table')->setAccessible(true); + $reflectUser->getProperty('idKey')->setAccessible(true); + $reflectUser->getProperty('uniqueKeyValues')->setAccessible(true); + $this->assertSame('usuari', $reflectUser->getProperty('table')->getValue($user)); + $this->assertSame('usuariId', $reflectUser->getProperty('idKey')->getValue($user)); + $this->assertSame(['usuariId', 'usuari', 'email'], $reflectUser->getProperty('uniqueKeyValues')->getValue($user)); + } + + public function testGetPrivateEmail(): void + { + $data = [ + 'email' => $this->testEmail, + ]; + $user = new User($data); + $this->assertSame($this->testEmail, $user->getPrivateEmail()); + } + + public function testGetNivell(): void + { + $data = [ + 'nivell' => $this->testNivell, + ]; + $user = new User($data); + $this->assertSame($this->testNivell, $user->getNivell()); + } + + /** + * @uses \PoolNET\config\Env + */ + public function testCheckPswd(): void + { + Env::executar(); + $hash = md5(getenv('ENV_ServerSalt') . $this->testSalt . $this->testPassword); + $data = [ + 'email' => $this->testEmail, + 'nivell' => $this->testNivell, + 'salt' => $this->testSalt, + 'hash' => $hash + ]; + $user = new User($data); + $this->assertTrue($user->checkPswd($this->testPassword)); + $this->assertFalse($user->checkPswd($this->testPassword . 'extra')); + } +} diff --git a/src/__tests__/models/errors/ForbiddenTest.php b/src/__tests__/models/errors/ForbiddenTest.php new file mode 100644 index 0000000..26f2790 --- /dev/null +++ b/src/__tests__/models/errors/ForbiddenTest.php @@ -0,0 +1,22 @@ +assertSame('Forbidden', $exception->getMessage()); + $this->assertSame(403, $exception->getCode()); + } +} diff --git a/src/__tests__/models/errors/InvalidJwtTokenTest.php b/src/__tests__/models/errors/InvalidJwtTokenTest.php new file mode 100644 index 0000000..dfef187 --- /dev/null +++ b/src/__tests__/models/errors/InvalidJwtTokenTest.php @@ -0,0 +1,19 @@ +assertSame('Invalid JWT token', $exception->getMessage()); + $this->assertSame(400, $exception->getCode()); + } +} diff --git a/src/__tests__/models/errors/InvalidUniqueKeyTest.php b/src/__tests__/models/errors/InvalidUniqueKeyTest.php new file mode 100644 index 0000000..6a1ada8 --- /dev/null +++ b/src/__tests__/models/errors/InvalidUniqueKeyTest.php @@ -0,0 +1,19 @@ +assertSame('Invalid unique key', $exception->getMessage()); + $this->assertSame(400, $exception->getCode()); + } +} diff --git a/src/__tests__/services/JwtHandlerTest.php b/src/__tests__/services/JwtHandlerTest.php new file mode 100644 index 0000000..8290fd0 --- /dev/null +++ b/src/__tests__/services/JwtHandlerTest.php @@ -0,0 +1,75 @@ +getProperty($property); + $reflectionProperty->setAccessible(true); + return $reflectionProperty->getValue((object) $reflectionClass->newInstance()); + } + /** + * @covers \PoolNET\service\JwtHandler::__construct + * @uses \PoolNET\config\Env + */ + public function testConstructor(): void + { + date_default_timezone_set('Europe/Berlin'); + $this->assertSame('Europe/Berlin', date_default_timezone_get()); + // Test that the constructor sets the correct issuedAt value + $this->assertSame(time(), $this->getObjectProtectedProperty('issuedAt')); + // Test that the constructor sets the correct expire value + $this->assertSame(time() + 3600, $this->getObjectProtectedProperty('expire')); + // Test that the constructor sets the correct jwtSecret value + $this->assertSame((string) getenv('ENV_JWTSecret'), $this->getObjectProtectedProperty('jwtSecret')); + } + /** + * @covers \PoolNET\service\JwtHandler::jwtEncodeData + * @uses \PoolNET\config\Env + */ + public function testJwtEncodeData(): void + { + $jwtHandler = new JwtHandler(); + $data = ['foo' => 'bar']; + $token = $jwtHandler->jwtEncodeData('poolnet', $data); + $this->assertIsString($token); + } + + /** + * @covers \PoolNET\service\JwtHandler::jwtDecodeData + * @uses \PoolNET\config\Env + * @uses \PoolNET\error\InvalidJwtToken + */ + public function testJwtDecodeData(): void + { + // Testejant l'èxit + $jwtHandler = new JwtHandler(); + $token = $jwtHandler->jwtEncodeData('poolnet', ['foo' => 'bar']); + $decoded = $jwtHandler->jwtDecodeData($token); + $this->assertSame('bar', $decoded->foo); + + // Testejant l'error + $this->expectException('\PoolNET\error\InvalidJwtToken'); + $this->expectExceptionMessage('Invalid JWT token'); + $invalidToken = "thisIsARandomStringWhichIsNotAValidJWT"; + $jwtHandler->jwtDecodeData($invalidToken); + } +} diff --git a/src/__tests__/services/MiddlewareArrayTest.php b/src/__tests__/services/MiddlewareArrayTest.php new file mode 100644 index 0000000..7d0d237 --- /dev/null +++ b/src/__tests__/services/MiddlewareArrayTest.php @@ -0,0 +1,32 @@ +assertInstanceOf(MiddlewareArray::class, $mwArray); + $this->assertCount(0, $mwArray); + + /** @var LoginValidator $mw */ + $mw = $this->createMock(LoginValidator::class); + $mwArray->add($mw); + $this->assertCount(1, $mwArray); + foreach ($mwArray as $mw) { + $this->assertInstanceOf(Middleware::class, $mw); + } + } +} diff --git a/src/__tests__/services/PageTest.php b/src/__tests__/services/PageTest.php new file mode 100644 index 0000000..24d8586 --- /dev/null +++ b/src/__tests__/services/PageTest.php @@ -0,0 +1,23 @@ +assertInstanceOf(Page::class, $page); + } +} diff --git a/src/__tests__/services/RouterJsonTest.php b/src/__tests__/services/RouterJsonTest.php new file mode 100644 index 0000000..cb7eb82 --- /dev/null +++ b/src/__tests__/services/RouterJsonTest.php @@ -0,0 +1,125 @@ +getRouterProtectedProperties(); + + $rutaTest = "/test"; + $router = new RouterJson($rutaTest); + $this->assertInstanceOf(RouterJson::class, $router); + $this->assertIsArray($this->controllersProp->getValue($router)); + $this->assertEquals(0, count($this->controllersProp->getValue($router))); + + $crudMock = $this->getCrudMock(); + + $router->get("/", $crudMock); + $this->assertEquals(1, count($this->controllersProp->getValue($router)["/"])); + + $router->post("/", $crudMock); + $this->assertEquals(2, count($this->controllersProp->getValue($router)["/"])); + + $router->patch("/", $crudMock); + $this->assertEquals(3, count($this->controllersProp->getValue($router)["/"])); + + $router->delete("/", $crudMock); + $this->assertEquals(4, count($this->controllersProp->getValue($router)["/"])); + + $rutaProp = $this->controllersProp->getValue($router)["/"]; + + $this->assertEquals($crudMock, $rutaProp["get"]["controller"]); + $this->assertEquals($crudMock, $rutaProp["post"]["controller"]); + $this->assertEquals($crudMock, $rutaProp["patch"]["controller"]); + $this->assertEquals($crudMock, $rutaProp["delete"]["controller"]); + } + /** + * @covers ::use + * @covers ::useController + * @covers ::useMethod + * @uses \PoolNET\service\Router + * @uses \PoolNET\service\RouterJson + * @uses \PoolNET\config\Request + * @uses \PoolNET\config\Response + * */ + public function testUse(): void + { + $router = new RouterJson("/test"); + + $crudMock = $this->getCrudMock(); + $router->get("", $crudMock); + + // Test exit. Es crida el controlador afegit en aquesta ruta amb aquest mètode. El retorn és false ja que no ha de seguir buscant ruta/controlador + $this->newReqRes(); + $req = $this->req->withPath("/test"); + $res = $this->res; + $this->assertFalse($router->use($req, $res)); + + // Test error. Ruta trobada, mètode incorrecte. + $this->newReqRes(); + $req = $this->req->withPath("/test")->withMethod("post"); + $res = $this->res; + ob_start(); + $this->assertFalse($router->use($req, $res)); + $output = json_decode(ob_get_contents(), true); + $this->assertArrayHasKey("error", $output); + $this->assertEquals("Mètode no permès", $output["error"]); + $this->assertEquals(405, http_response_code()); + ob_end_clean(); + + // Test error. Ruta no trobada. + $this->newReqRes(); + $req = $this->req->withPath("/testError"); + $res = $this->res; + ob_start(); + $this->assertTrue($router->use($req, $res)); + $output = json_decode(ob_get_contents(), true); + $this->assertArrayHasKey("error", $output); + $this->assertEquals("Ruta no trobada", $output["error"]); + $this->assertEquals(404, http_response_code()); + ob_end_clean(); + } + /** + * @coversNothing + * @doesNotPerformAssertions + */ + private function getRouterProtectedProperties(): void + { + $reflectionClass = new ReflectionClass(RouterJson::class); + $this->controllersProp = $reflectionClass->getProperty('controllers'); + $this->controllersProp->setAccessible(true); + } + private function getCrudMock(): CRUD + { + $crudMock = $this->createMock(CRUD::class); + $crudMock->method('get')->with($this->isInstanceOf(Request::class), $this->isInstanceOf(Response::class)); + $crudMock->method('post')->with($this->isInstanceOf(Request::class), $this->isInstanceOf(Response::class)); + $crudMock->method('patch')->with($this->isInstanceOf(Request::class), $this->isInstanceOf(Response::class)); + $crudMock->method('delete')->with($this->isInstanceOf(Request::class), $this->isInstanceOf(Response::class)); + /** @var CRUD $crudMock */ + return $crudMock; + } +} diff --git a/src/__tests__/services/RouterPageTest.php b/src/__tests__/services/RouterPageTest.php new file mode 100644 index 0000000..12eaf8d --- /dev/null +++ b/src/__tests__/services/RouterPageTest.php @@ -0,0 +1,49 @@ +getRouterProtectedProperties(); + + $rutaUsuari = "/usuari"; + $router = new RouterPage($rutaUsuari); + $this->assertInstanceOf(RouterPage::class, $router); + $this->assertInstanceOf(stdClass::class, $this->pagesProp->getValue($router)); + $this->assertEquals(0, count(get_object_vars($this->pagesProp->getValue($router)))); + + $page = new Page("Usuari"); + $router->addPage($rutaUsuari, $page); + $this->assertEquals(1, count(get_object_vars($this->pagesProp->getValue($router)))); + $this->assertInstanceOf(Page::class, $this->pagesProp->getValue($router)->{$rutaUsuari}); + $this->assertEquals($page, $this->pagesProp->getValue($router)->{$rutaUsuari}); + } + /** + * @coversNothing + * @doesNotPerformAssertions + */ + private function getRouterProtectedProperties(): void + { + $reflectionClass = new ReflectionClass(RouterPage::class); + $this->pagesProp = $reflectionClass->getProperty('pages'); + $this->pagesProp->setAccessible(true); + } +} diff --git a/src/__tests__/services/RouterTest.php b/src/__tests__/services/RouterTest.php new file mode 100644 index 0000000..de3eef7 --- /dev/null +++ b/src/__tests__/services/RouterTest.php @@ -0,0 +1,158 @@ +getRouterProtectedProperties(); + + // Amb valors per defecte + $router = new Router(); + $this->assertInstanceOf(Router::class, $router); + $this->assertNull($router->prefix); + $this->assertEquals('json', $this->formatProp->getValue($router)); + $this->assertInstanceOf(stdClass::class, $this->routersProp->getValue($router)); + $this->assertEquals(0, count(get_object_vars($this->routersProp->getValue($router)))); + + // Amb valors predefinits + $router2 = new Router(("/main/accio"), "html"); + $this->assertInstanceOf(Router::class, $router2); + $this->assertEquals("/main/accio", $router2->prefix); + $this->assertEquals('html', $this->formatProp->getValue($router2)); + $this->assertInstanceOf(stdClass::class, $this->routersProp->getValue($router2)); + $this->assertEquals(0, count(get_object_vars($this->routersProp->getValue($router2)))); + } + /** + * @covers ::addRouter + * @uses \PoolNET\service\Router + * @uses \PoolNET\service\RouterPage + */ + public function testAddRouter(): void + { + $pageRoute = "/pages"; + $this->getRouterProtectedProperties(); + $router1 = new Router("/", "json"); + $router2 = new RouterPage($pageRoute); + + // No hi ha cap router d'inici + $this->assertInstanceOf(stdClass::class, $this->routersProp->getValue($router1)); + $this->assertEquals(0, count(get_object_vars($this->routersProp->getValue($router1)))); + + // N'afageixo un, hi és, i és del tipus que toca. + $router1->addRouter($pageRoute, $router2); + $this->assertEquals(1, count(get_object_vars($this->routersProp->getValue($router1)))); + $this->assertInstanceOf(Router::class, $this->routersProp->getValue($router1)->{$pageRoute}); + $this->assertEquals($router2, $this->routersProp->getValue($router1)->{$pageRoute}); + } + /** + * @covers ::use + * @covers ::useRouter + * @uses \PoolNET\service\Router + * @uses \PoolNET\config\Request + */ + public function testUse(): void + { + // Test exit. Es crida el router afegit. + $this->newReqRes(); + $req = $this->req->withPath("/mock/test"); + $res = $this->res; + + $router = new Router("/mock"); + $routerMock = $this->createMock(Router::class); + $routerMock->prefix = "/test"; + $routerMock->expects($this->once())->method('use')->with($this->isInstanceOf(Request::class), $this->isInstanceOf(Response::class)); + /** @var Router $routerMock */ + $router->addRouter("/test", $routerMock); + $this->assertTrue($router->use($req, $res)); + + // Test false + $this->newReqRes(); + $req = $this->req->withPath("/api/non/existent"); + $res = $this->res; + $router = new Router(); + $this->assertFalse($router->use($req, $res)); + } + /** + * @covers ::removePrefix + * @uses \PoolNET\service\Router + */ + public function testRemovePrefix(): void + { + $prefix = "/api/accio"; + $router = new Router($prefix); + $reflectionClass = new ReflectionClass(Router::class); + $removePrefix = $reflectionClass->getMethod('removePrefix'); + $removePrefix->setAccessible(true); + // Resultat esperat + $result1 = $removePrefix->invoke($router, "/api/accio/edit/123"); + $this->assertEquals("/edit/123", $result1); + // El prefix no hi és, no cal treure res + $route = "/page/control"; + $result2 = $removePrefix->invoke($router, $route); + $this->assertEquals($route, $result2); + } + /** + * @covers ::removeClosingSlash + * @uses \PoolNET\service\Router + */ + public function testRemoveClosingSlash(): void + { + $router = new Router(); + $reflectionClass = new ReflectionClass(Router::class); + $removeClosingSlash = $reflectionClass->getMethod('removeClosingSlash'); + $removeClosingSlash->setAccessible(true); + // Resultat esperat + $result1 = $removeClosingSlash->invoke($router, "/route/to/api/"); + $this->assertEquals("/route/to/api", $result1); + // Manté la barra si només conté la barra + $result2 = $removeClosingSlash->invoke($router, "/"); + $this->assertEquals("/", $result2); + // No elimina res si no hi ha barra final + $route = "/normal/route"; + $result3 = $removeClosingSlash->invoke($router, $route); + $this->assertEquals($route, $result3); + } + /** + * @covers ::getSuccessiveRoutes + * @uses \PoolNET\service\Router + */ + public function testGetSuccessiveRoutes(): void + { + $router = new Router(); + $reflectionClass = new ReflectionClass(Router::class); + $getSuccessiveRoutes = $reflectionClass->getMethod('getSuccessiveRoutes'); + $getSuccessiveRoutes->setAccessible(true); + // Resultat esperat + $result1 = $getSuccessiveRoutes->invoke($router, "/route/to/api"); + $this->assertEquals(["/", "/route", "/route/to", "/route/to/api"], $result1); + } + /** + * @coversNothing + * @doesNotPerformAssertions + */ + private function getRouterProtectedProperties(): void + { + $reflectionClass = new ReflectionClass(Router::class); + $this->formatProp = $reflectionClass->getProperty('format'); + $this->formatProp->setAccessible(true); + $this->routersProp = $reflectionClass->getProperty('routers'); + $this->routersProp->setAccessible(true); + } +} diff --git a/src/composer.json b/src/composer.json new file mode 100644 index 0000000..ef09ad2 --- /dev/null +++ b/src/composer.json @@ -0,0 +1,29 @@ +{ + "require": { + "firebase/php-jwt": "^6.5" + }, + "autoload": { + "psr-4": { + "PoolNET\\": "models/", + "PoolNET\\config\\": "config/", + "PoolNET\\interface\\": "interfaces/", + "PoolNET\\MW\\": "middlewares/", + "PoolNET\\error\\": "models/errors/", + "PoolNET\\service\\": "services/", + "PoolNET\\controller\\": "controllers/", + "PoolNET\\route\\": "routes/", + "PoolNET\\page\\": "pages/" + }, + "classmap": ["interfaces/"], + "files": [ + "routes/index.php", + "pages/index.php" + ] + }, + "require-dev": { + "phpunit/phpunit": "^10.2" + }, + "autoload-dev": { + "classmap": ["__tests__/"] + } +} diff --git a/back/composer.lock b/src/composer.lock similarity index 86% rename from back/composer.lock rename to src/composer.lock index c964b77..dc652ec 100644 --- a/back/composer.lock +++ b/src/composer.lock @@ -8,26 +8,26 @@ "packages": [ { "name": "firebase/php-jwt", - "version": "v6.5.0", + "version": "v6.10.1", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "e94e7353302b0c11ec3cfff7180cd0b1743975d2" + "reference": "500501c2ce893c824c801da135d02661199f60c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/e94e7353302b0c11ec3cfff7180cd0b1743975d2", - "reference": "e94e7353302b0c11ec3cfff7180cd0b1743975d2", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5", + "reference": "500501c2ce893c824c801da135d02661199f60c5", "shasum": "" }, "require": { - "php": "^7.4||^8.0" + "php": "^8.0" }, "require-dev": { - "guzzlehttp/guzzle": "^6.5||^7.4", + "guzzlehttp/guzzle": "^7.4", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "psr/cache": "^1.0||^2.0", + "psr/cache": "^2.0||^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0" }, @@ -65,24 +65,24 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.5.0" + "source": "https://github.com/firebase/php-jwt/tree/v6.10.1" }, - "time": "2023-05-12T15:47:07+00:00" + "time": "2024-05-18T18:05:11+00:00" } ], "packages-dev": [ { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -90,11 +90,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -120,7 +121,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -128,29 +129,31 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nikic/php-parser", - "version": "v4.16.0", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17" + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -158,7 +161,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -182,26 +185,27 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" }, - "time": "2023-06-25T14:52:30+00:00" + "time": "2024-07-01T20:03:41+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -242,9 +246,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -299,23 +309,23 @@ }, { "name": "phpunit/php-code-coverage", - "version": "10.1.2", + "version": "10.1.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e" + "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/db1497ec8dd382e82c962f7abbe0320e4882ee4e", - "reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae", + "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=8.1", "phpunit/php-file-iterator": "^4.0", "phpunit/php-text-template": "^3.0", @@ -365,7 +375,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.2" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.15" }, "funding": [ { @@ -373,20 +383,20 @@ "type": "github" } ], - "time": "2023-05-22T09:04:27+00:00" + "time": "2024-06-29T08:25:15+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "4.0.2", + "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "5647d65443818959172645e7ed999217360654b6" + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/5647d65443818959172645e7ed999217360654b6", - "reference": "5647d65443818959172645e7ed999217360654b6", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", "shasum": "" }, "require": { @@ -426,7 +436,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.0.2" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" }, "funding": [ { @@ -434,7 +444,7 @@ "type": "github" } ], - "time": "2023-05-07T09:13:23+00:00" + "time": "2023-08-31T06:24:48+00:00" }, { "name": "phpunit/php-invoker", @@ -501,16 +511,16 @@ }, { "name": "phpunit/php-text-template", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "9f3d3709577a527025f55bcf0f7ab8052c8bb37d" + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/9f3d3709577a527025f55bcf0f7ab8052c8bb37d", - "reference": "9f3d3709577a527025f55bcf0f7ab8052c8bb37d", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", "shasum": "" }, "require": { @@ -548,7 +558,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.0" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" }, "funding": [ { @@ -556,7 +567,7 @@ "type": "github" } ], - "time": "2023-02-03T06:56:46+00:00" + "time": "2023-08-31T14:07:24+00:00" }, { "name": "phpunit/php-timer", @@ -619,16 +630,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.2.4", + "version": "10.5.27", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "68484779b5a2ed711fbdeba6ca01910d87acdff2" + "reference": "2425f713b2a5350568ccb1a2d3984841a23e83c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/68484779b5a2ed711fbdeba6ca01910d87acdff2", - "reference": "68484779b5a2ed711fbdeba6ca01910d87acdff2", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2425f713b2a5350568ccb1a2d3984841a23e83c5", + "reference": "2425f713b2a5350568ccb1a2d3984841a23e83c5", "shasum": "" }, "require": { @@ -638,26 +649,26 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=8.1", - "phpunit/php-code-coverage": "^10.1.1", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-invoker": "^4.0", - "phpunit/php-text-template": "^3.0", - "phpunit/php-timer": "^6.0", - "sebastian/cli-parser": "^2.0", - "sebastian/code-unit": "^2.0", - "sebastian/comparator": "^5.0", - "sebastian/diff": "^5.0", - "sebastian/environment": "^6.0", - "sebastian/exporter": "^5.0", - "sebastian/global-state": "^6.0", - "sebastian/object-enumerator": "^5.0", - "sebastian/recursion-context": "^5.0", - "sebastian/type": "^4.0", - "sebastian/version": "^4.0" + "phpunit/php-code-coverage": "^10.1.15", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.1", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.2", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.0", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" }, "suggest": { "ext-soap": "To be able to generate mocks based on WSDL files" @@ -668,7 +679,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.2-dev" + "dev-main": "10.5-dev" } }, "autoload": { @@ -700,7 +711,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.2.4" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.27" }, "funding": [ { @@ -716,20 +727,20 @@ "type": "tidelift" } ], - "time": "2023-07-10T04:06:08+00:00" + "time": "2024-07-10T11:48:06+00:00" }, { "name": "sebastian/cli-parser", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae" + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/efdc130dbbbb8ef0b545a994fd811725c5282cae", - "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", "shasum": "" }, "require": { @@ -764,7 +775,8 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.0" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" }, "funding": [ { @@ -772,7 +784,7 @@ "type": "github" } ], - "time": "2023-02-03T06:58:15+00:00" + "time": "2024-03-02T07:12:49+00:00" }, { "name": "sebastian/code-unit", @@ -887,16 +899,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.0", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "72f01e6586e0caf6af81297897bd112eb7e9627c" + "reference": "2db5010a484d53ebf536087a70b4a5423c102372" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/72f01e6586e0caf6af81297897bd112eb7e9627c", - "reference": "72f01e6586e0caf6af81297897bd112eb7e9627c", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372", + "reference": "2db5010a484d53ebf536087a70b4a5423c102372", "shasum": "" }, "require": { @@ -907,7 +919,7 @@ "sebastian/exporter": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.3" }, "type": "library", "extra": { @@ -951,7 +963,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.0" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.1" }, "funding": [ { @@ -959,24 +972,24 @@ "type": "github" } ], - "time": "2023-02-03T07:07:16+00:00" + "time": "2023-08-14T13:18:12+00:00" }, { "name": "sebastian/complexity", - "version": "3.0.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "e67d240970c9dc7ea7b2123a6d520e334dd61dc6" + "reference": "68ff824baeae169ec9f2137158ee529584553799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/e67d240970c9dc7ea7b2123a6d520e334dd61dc6", - "reference": "e67d240970c9dc7ea7b2123a6d520e334dd61dc6", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", "shasum": "" }, "require": { - "nikic/php-parser": "^4.10", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=8.1" }, "require-dev": { @@ -985,7 +998,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.2-dev" } }, "autoload": { @@ -1008,7 +1021,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/3.0.0" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" }, "funding": [ { @@ -1016,20 +1030,20 @@ "type": "github" } ], - "time": "2023-02-03T06:59:47+00:00" + "time": "2023-12-21T08:37:17+00:00" }, { "name": "sebastian/diff", - "version": "5.0.3", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b" + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/912dc2fbe3e3c1e7873313cc801b100b6c68c87b", - "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", "shasum": "" }, "require": { @@ -1037,12 +1051,12 @@ }, "require-dev": { "phpunit/phpunit": "^10.0", - "symfony/process": "^4.2 || ^5" + "symfony/process": "^6.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -1075,7 +1089,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" }, "funding": [ { @@ -1083,20 +1097,20 @@ "type": "github" } ], - "time": "2023-05-01T07:48:21+00:00" + "time": "2024-03-02T07:15:17+00:00" }, { "name": "sebastian/environment", - "version": "6.0.1", + "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951" + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951", - "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", "shasum": "" }, "require": { @@ -1111,7 +1125,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -1139,7 +1153,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" }, "funding": [ { @@ -1147,20 +1161,20 @@ "type": "github" } ], - "time": "2023-04-11T05:39:26+00:00" + "time": "2024-03-23T08:47:14+00:00" }, { "name": "sebastian/exporter", - "version": "5.0.0", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "f3ec4bf931c0b31e5b413f5b4fc970a7d03338c0" + "reference": "955288482d97c19a372d3f31006ab3f37da47adf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/f3ec4bf931c0b31e5b413f5b4fc970a7d03338c0", - "reference": "f3ec4bf931c0b31e5b413f5b4fc970a7d03338c0", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf", "shasum": "" }, "require": { @@ -1174,7 +1188,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -1216,7 +1230,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.0.0" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" }, "funding": [ { @@ -1224,20 +1239,20 @@ "type": "github" } ], - "time": "2023-02-03T07:06:49+00:00" + "time": "2024-03-02T07:17:12+00:00" }, { "name": "sebastian/global-state", - "version": "6.0.0", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "aab257c712de87b90194febd52e4d184551c2d44" + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/aab257c712de87b90194febd52e4d184551c2d44", - "reference": "aab257c712de87b90194febd52e4d184551c2d44", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "shasum": "" }, "require": { @@ -1271,13 +1286,14 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.0" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" }, "funding": [ { @@ -1285,24 +1301,24 @@ "type": "github" } ], - "time": "2023-02-03T07:07:38+00:00" + "time": "2024-03-02T07:19:19+00:00" }, { "name": "sebastian/lines-of-code", - "version": "2.0.0", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "17c4d940ecafb3d15d2cf916f4108f664e28b130" + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/17c4d940ecafb3d15d2cf916f4108f664e28b130", - "reference": "17c4d940ecafb3d15d2cf916f4108f664e28b130", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", "shasum": "" }, "require": { - "nikic/php-parser": "^4.10", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=8.1" }, "require-dev": { @@ -1334,7 +1350,8 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.0" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" }, "funding": [ { @@ -1342,7 +1359,7 @@ "type": "github" } ], - "time": "2023-02-03T07:08:02+00:00" + "time": "2023-12-21T08:38:20+00:00" }, { "name": "sebastian/object-enumerator", @@ -1630,16 +1647,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -1668,7 +1685,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -1676,7 +1693,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], diff --git a/src/config/Request.php b/src/config/Request.php new file mode 100644 index 0000000..595dd15 --- /dev/null +++ b/src/config/Request.php @@ -0,0 +1,94 @@ +uri = $this->getUri(); + $this->headers = $this->getHeaders(); + $this->cookieParams = $_COOKIE; + $this->method = strtolower($_SERVER['REQUEST_METHOD']); + $this->body = $this->parseBody(); + } + + public function getUri(): string + { + return $_SERVER['REQUEST_URI']; + } + public function getPath(): string + { + $route = explode("/PoolNET", $this->uri)[1]; + return explode('?', $route)[0]; + } + public function withPath(string $path): self + { + $this->uri = "/PoolNET" . $path; + return $this; + } + public function getParams(): string + { + return explode('?', $this->uri)[1] ?? ""; + } + public function getMethod(): string + { + return $this->method; + } + public function withMethod(string $method): self + { + $this->method = strtolower($method); + return $this; + } + public function getHeaders(): array + { + // return apache_request_headers(); + $headers = array(); + foreach ($_SERVER as $k => $v) { + if (substr($k, 0, 5) == "HTTP_") { + $k = str_replace('_', ' ', substr($k, 5)); + $k = str_replace(' ', '-', ucwords(strtolower($k))); + $headers[$k] = $v; + } else { + $k = str_replace('_', ' ', $k); + $k = str_replace(' ', '-', ucwords(strtolower($k))); + $headers[$k] = $v; + } + } + return $headers; + } + public function getCookieParams(): array + { + return $this->cookieParams; + } + public function withCookieParams(array $cookieParams): self + { + foreach ($cookieParams as $key => $value) { + $this->cookieParams[$key] = $value; + } + return $this; + } + public function getParsedBody(): array + { + return $this->body ? $this->body : []; + } + private function parseBody(): ?array + { + if (isset($this->headers["Content-Type"]) && $this->headers["Content-Type"] === "application/x-www-form-urlencoded") { + return $_POST; + } + return json_decode(file_get_contents('php://input'), true); + } + public function withBody(array $body): self + { + $this->body = $body; + return $this; + } +} diff --git a/src/config/Response.php b/src/config/Response.php new file mode 100644 index 0000000..94becc3 --- /dev/null +++ b/src/config/Response.php @@ -0,0 +1,70 @@ +headers[$name] = is_string($value) ? $value : implode(',', $value); + return $this; + } + private function sendHeaders(): void + { + foreach ($this->headers as $name => $value) { + header(sprintf('%s: %s', $name, $value)); + } + } + + public function withStatus(int $code): self + { + $this->status = $code; + return $this; + } + + public function toJson(?array $data): void + { + $this->withHeader('Content-Type', 'application/json'); + $this->sendHeaders(); + $data ??= $this->defaultResponse(); + http_response_code($this->status); + echo json_encode($data); + } + private function defaultResponse(): array + { + $response = array(); + switch ($this->status) { + case 405: + $response = ["error" => "Mètode no permès"]; + break; + case 500: + default: + $response = ["error" => "Alguna cosa ha fallat"]; + break; + } + return $response; + } + public function handleError(Throwable $th): void + { + switch ($th) { + case $th instanceof PDOException: + // TODO: Log error per a us intern + $this->withStatus(500)->toJson(['error' => "Error amb la base de dades"]); + break; + case $th instanceof Forbidden: + $this->withStatus(403)->toJson(['error' => "No tens permisos per aquesta acció"]); + break; + default: + $this->withStatus($th->getCode())->toJson(['error' => "Alguna cosa ha fallat"]); + break; + } + } +} diff --git a/src/config/Session.php b/src/config/Session.php new file mode 100644 index 0000000..81399bc --- /dev/null +++ b/src/config/Session.php @@ -0,0 +1,38 @@ +jwtHandler = new JwtHandler(); + $this->tokenCookie = $_COOKIE['token'] ?? null; + + } + + private function getTokenData() + { + try { + $this->tokenData = $this->jwtHandler->jwtDecodeData($this->tokenCookie); + $this->loggedInUser = true; + } catch (InvalidJwtToken $err) { + $this->tokenData = null; + $this->loggedInUser = false; + } + } + private function setUser() + { + + } +} diff --git a/src/config/database/Database.php b/src/config/database/Database.php new file mode 100644 index 0000000..948a742 --- /dev/null +++ b/src/config/database/Database.php @@ -0,0 +1,37 @@ +dbName = (string) getenv('ENV_DB_NAME'); + } + /** + * Connecta a la base de dades + * @return PDO La connexió a la base de dades + * @throws PDOException + */ + public function connect(): PDO | null + { + // try { + $this->dbcnx = new PDO( + // 'mysql:host=' . $this->host . ';dbname=' . $this->dbName, + dsn: 'sqlite:' . __DIR__ . '/' . $this->dbName . '.db', + options: [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] // Això per poder capturar errors diria. + ); + // } catch (PDOException $err) { + // echo 'Database connection failed: ' . $err->getMessage(); + // return null; + // } + return $this->dbcnx; + } +} diff --git a/src/config/database/database.sql b/src/config/database/database.sql new file mode 100644 index 0000000..dfaa70c --- /dev/null +++ b/src/config/database/database.sql @@ -0,0 +1,36 @@ +CREATE TABLE `usuari` ( + `usuariId` int(11) NOT NULL PRIMARY KEY, + `usuari` varchar(20) NOT NULL, + `email` varchar(50) NOT NULL, + `salt` varchar(20) NOT NULL, + `hash` char(32) NOT NULL, + `nivell` tinyint(4) NOT NULL DEFAULT 2, + `data_creacio` date NOT NULL +); + +CREATE TABLE `control` ( + `controlId` int(11) NOT NULL PRIMARY KEY, + `data_hora` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `ph` decimal(3, 2) DEFAULT NULL, + `clor` decimal(3, 2) DEFAULT NULL, + `alcali` float DEFAULT NULL, + `temperatura` tinyint(4) DEFAULT NULL, + `transparent` tinyint(4) DEFAULT NULL, + `fons` tinyint(4) DEFAULT NULL, + `usuari` int(11) NOT NULL, + FOREIGN KEY (`usuari`) REFERENCES `usuari` (`usuariId`) ON UPDATE CASCADE ON DELETE RESTRICT +); + +CREATE TABLE `accio` ( + `accioId` int(11) NOT NULL PRIMARY KEY, + `data_hora` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `ph` tinyint(4) DEFAULT NULL, + `clor` tinyint(4) DEFAULT NULL, + `antialga` tinyint(4) DEFAULT NULL, + `fluoculant` tinyint(4) DEFAULT NULL, + `aspirar` tinyint(4) DEFAULT NULL, + `alcali` tinyint(4) DEFAULT NULL, + `aglutinant` tinyint(4) DEFAULT NULL, + `usuari` int(11) NOT NULL, + FOREIGN KEY (`usuari`) REFERENCES `usuari` (`usuariId`) ON UPDATE CASCADE ON DELETE RESTRICT +); diff --git a/src/config/database/db_seed_test.sql b/src/config/database/db_seed_test.sql new file mode 100644 index 0000000..0c85485 --- /dev/null +++ b/src/config/database/db_seed_test.sql @@ -0,0 +1,190 @@ +-- Seed +INSERT INTO + `usuari` ( + `usuariID`, + `usuari`, + `email`, + `salt`, + `hash`, + `nivell`, + `data_creacio` + ) +VALUES ( + 1, + 'Admin', + 'admin@example.cat', + 'Some salt', + '30fc87a9f1f0643c3169998da58d45f4', + 0, + '2023-07-21' + ), + ( + 2, + 'Test', + 'test@example.cat', + 'Salt', + 'a8b355d5a6ca4f80ce9fadf43ec56377', + 1, + '2023-07-21' + ); + +INSERT INTO + `control` ( + `controlId`, + `data_hora`, + `ph`, + `clor`, + `alcali`, + `temperatura`, + `transparent`, + `fons`, + `usuari` + ) +VALUES ( + 1, + '2022-11-15 18:47:10', + '6.80', + '3.00', + NULL, + NULL, + 1, + 2, + 1 + ), + ( + 2, + '2022-11-19 16:53:03', + '6.50', + '3.00', + 2, + NULL, + NULL, + NULL, + 1 + ), + ( + 3, + '2022-11-25 18:08:48', + '6.80', + '3.00', + NULL, + 16, + 1, + 2, + 1 + ), + ( + 4, + '2022-12-18 14:02:12', + '6.80', + '3.00', + NULL, + 12, + 1, + 2, + 2 + ), + ( + 5, + '2023-06-05 12:25:06', + NULL, + '7.20', + NULL, + NULL, + NULL, + NULL, + 1 + ), + ( + 6, + '2023-07-04 17:54:22', + '7.20', + '0.10', + NULL, + NULL, + NULL, + NULL, + 2 + ), + ( + 7, + '2023-07-04 18:00:39', + '8.10', + '0.10', + NULL, + 28, + 1, + 1, + 2 + ), + ( + 8, + '2023-07-04 18:01:52', + '8.10', + '0.10', + NULL, + 28, + 1, + 1, + 2 + ), + ( + 9, + '2023-07-07 17:01:00', + '7.20', + '0.10', + 1, + 28, + 1, + 1, + 1 + ), + ( + 10, + '2023-07-08 17:30:19', + '7.20', + '0.10', + 1, + 28, + 1, + 1, + 1 + ); + +INSERT INTO + `accio` ( + `accioId`, + `data_hora`, + `ph`, + `clor`, + `antialga`, + `fluoculant`, + `aspirar`, + `alcali`, + `aglutinant`, + `usuari` + ) +VALUES ( + 1, + '2022-09-01 00:00:00', + -1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ), + ( + 2, + '2022-11-25 18:18:49', + NULL, + NULL, + 1, + NULL, + NULL, + NULL, + NULL, + 1 + ); \ No newline at end of file diff --git a/back/config/env-example.php b/src/config/env-example.php similarity index 84% rename from back/config/env-example.php rename to src/config/env-example.php index 09e80ac..53e5e2e 100644 --- a/back/config/env-example.php +++ b/src/config/env-example.php @@ -10,10 +10,7 @@ class Env */ public static function executar(): void { - putenv('ENV_DB_HOST=localhost'); putenv('ENV_DB_NAME=PoolNET'); - putenv('ENV_DB_USER=root'); - putenv('ENV_DB_PSWD=1234'); putenv('ENV_HEADERS_ALLOW_ORIGIN=*'); putenv('ENV_HEADERS_ALLOW_HEADERS=*'); diff --git a/back/controllers/authLogin.php b/src/controllers/AuthLogin.php similarity index 60% rename from back/controllers/authLogin.php rename to src/controllers/AuthLogin.php index c28491d..6913935 100644 --- a/back/controllers/authLogin.php +++ b/src/controllers/AuthLogin.php @@ -1,31 +1,34 @@ $body El cos de la petició * @return void */ - public static function post(array $body): void + public function post(Request $req, Response $res): void { - parent::headers("POST"); + $body = $req->getParsedBody(); $user = User::trobarPerUnic('usuari', $body['usuari']); if (!$user || !$user->checkPswd($body['password'])) { - parent::respostaSimple(400, ["error" => "Error amb les credencials."], false); + $res->withStatus(400)->toJson(["error" => "Error amb les credencials."]); + return; } $jwt = new JwtHandler(); $token = $jwt->jwtEncodeData('piscina', [ - 'userID' => $user->userID, + 'usuariId' => $user->usuariId, 'usuari' => $user->usuari, - 'nivell' => $user->nivell, + 'nivell' => $user->getNivell(), 'email' => $user->getPrivateEmail(), ]); - http_response_code(200); setcookie("token", $token, [ "httpOnly" => true, "expires" => time() + (10 * 365 * 24 * 60 * 60), // 10 anys des d'ara @@ -33,7 +36,7 @@ public static function post(array $body): void // "secure" => true, // Només disponible a través de HTTPS "samesite" => "Strict", // Només disponible per al mateix lloc (no cross-site) ]); - echo json_encode(["token" => $token]); - exit; + $res->withHeader("Location", "/PoolNET"); + $res->withStatus(302)->toJson(["token" => $token]); } } diff --git a/src/controllers/Control.php b/src/controllers/Control.php new file mode 100644 index 0000000..b74ebe7 --- /dev/null +++ b/src/controllers/Control.php @@ -0,0 +1,100 @@ + ['data_hora', 'DESC']], 20); + $num = count($result); + $num > 0 ? $data = $result : $data = ['message' => 'No s\'ha trobat cap control']; + $res->withStatus(200)->toJson($data); + } + // /** + // * @param array $body El cos de la petició + // * @return void + // */ + // public function post(Request $req, Response $res): void + // { + // $body = $req->getParsedBody(); + // $userData = json_decode(getenv('JWT_USER_DATA')); + // $control = new DtoControl($body); + // $control->usuari = (int) $userData->usuariId; + // if ($control->allNull()) { + // $res->withStatus(400)->toJson(["error" => "Mínim has d'omplir un camp."]); + // } + // if ($control->desar()) { + // $res->withStatus(201)->toJson([]); + // } else { + // $res->withStatus(500)->toJson(["error" => "No s'ha pogut desar el control de l'aigua."]); + // } + // } + // /** + // * @param array $body El cos de la petició + // * @return void + // */ + // public static function patch(array $body): void + // { + // // parent::headers("PATCH"); + // try { + // $userData = json_decode(getenv('JWT_USER_DATA')); + // $controlAEditar = DtoControl::trobarPerUnic('controlId', (int) $body['controlId']); + // if ($controlAEditar === null) { + // // parent::respostaSimple(404, ["error" => "No s'ha trobat el control."], false); + // } + // $controlAEditar->getDadesUsuari(); + // if ($controlAEditar->user->usuariId != (int) $userData->usuariId && (int) $userData->nivell > 0) { + // // parent::respostaSimple(403, ["error" => "Només pots editar controls propis."], false); + // } + // foreach ($body as $camp => $valor) { + // $controlAEditar->$camp = $valor; + // } + // if ($controlAEditar->allNull()) { + // // parent::respostaSimple(400, ["error" => "No pots buidar un control."], false); + // } + // if ($controlAEditar->desar()) { + // // parent::respostaSimple(204, null, false); + // } else { + // // parent::respostaSimple(500, ["error" => "No s'ha pogut desar el control."], false); + // } + // } catch (Throwable $th) { + // // parent::respostaSimple(400, ["error" => $th->getMessage()], false); + // } + // } + // /** + // * @param array $body El cos de la petició + // * @return void + // */ + // public static function delete(array $body): void + // { + // // parent::headers("DELETE"); + // try { + // $userData = json_decode(getenv('JWT_USER_DATA')); + // $controlAEliminar = DtoControl::trobarPerUnic('controlId', (int) $body['controlId']); + // if ($controlAEliminar === null) { + // // parent::respostaSimple(404, ["error" => "No s'ha trobat el control."], false); + // } + // $controlAEliminar->getDadesUsuari(); + // if ($controlAEliminar->user->usuariId != (int) $userData->usuariId && (int) $userData->nivell > 0) { + // // parent::respostaSimple(403, ["error" => "Només pots eliminar controls propis."], false); + // } + // if ($controlAEliminar->borrar()) { + // // parent::respostaSimple(204, null, false); + // } else { + // // parent::respostaSimple(500, ["error" => "No s'ha pogut borrar el control."], false); + // } + // } catch (Throwable $th) { + // // parent::respostaSimple(400, ["error" => $th->getMessage()], false); + // } + // } +} diff --git a/src/interfaces/Controller.php b/src/interfaces/Controller.php new file mode 100644 index 0000000..db3d54e --- /dev/null +++ b/src/interfaces/Controller.php @@ -0,0 +1,24 @@ +jwt = new JwtHandler(); + } + public function use(Request &$req, Response &$res): bool + { + $jwt_token = $req->getCookieParams()['token'] ?? null; + if ($jwt_token === null) { + throw new Forbidden(); + } + $data = $this->jwt->jwtDecodeData($jwt_token); + if (!isset($data->usuariId)) { + throw new Forbidden(); + } + $user = User::trobarPerId($data->usuariId); + if ($user === null) { + throw new Forbidden(); + } + if ($user->getNivell() < $this->nivell) { + throw new Forbidden(); + } + // TODO: Posar l'usuari al req + return true; + } +} diff --git a/src/middlewares/LoginValidator.php b/src/middlewares/LoginValidator.php new file mode 100644 index 0000000..5d61950 --- /dev/null +++ b/src/middlewares/LoginValidator.php @@ -0,0 +1,15 @@ +requiredFields($req, $res, ["usuari" => "string", "password" => "string"]); + } +} diff --git a/back/middlewares/Validator.php b/src/middlewares/Validator.php similarity index 57% rename from back/middlewares/Validator.php rename to src/middlewares/Validator.php index 2c0cdf4..a70cd7d 100644 --- a/back/middlewares/Validator.php +++ b/src/middlewares/Validator.php @@ -1,43 +1,42 @@ |null $obligatori [Opcional] Valors necessaris que han der ser al cos de la petició i el seu tipus. Per exemple, ``['controlID' => 'integer']``. - * @return array Cos de la petició parsejat. + * @param Request $req + * @param array|null $obligatori [Opcional] Valors necessaris que han der ser al cos de la petició i el seu tipus. Per exemple, ``['controlId' => 'integer']``. + * @return bool */ - public static function parseBody( ? array $obligatori = null) : array + public function requiredFields(Request &$req, Response &$res, array $obligatori): bool { - $body = json_decode(file_get_contents('php://input'), true); + $body = $req->getParsedBody(); if ($obligatori !== null) { foreach ($obligatori as $param => $tipus) { if (isset($body[$param])) { if (gettype($body[$param]) !== $tipus) { - self::respostaSimple( - 400, - [ - "error" => "Algun camp no és del tipus correcte.", - "camps_obligatoris" => $obligatori, - ], - ); + $res->withStatus(400)->toJson([ + "error" => "Algun camp no és del tipus correcte.", + "camps_obligatoris" => $obligatori, + ]); + return false; } } else { - self::respostaSimple( - 400, - [ - "error" => "Falta algun camp obligatori.", - "camps_obligatoris" => $obligatori, - ], - ); + $res->withStatus(400)->toJson([ + "error" => "Falta algun camp obligatori.", + "camps_obligatoris" => $obligatori, + "enviat" => $body + ]); + return false; } } } - return $body; + return true; } /** * Valida els valors del cos de la petició amb els tipus que admet la classe passada com a paràmetre. @@ -45,7 +44,7 @@ public static function parseBody( ? array $obligatori = null) : array * @param string $class Classe que ha de validar els paràmetres. * @return void Respon amb 400 si algun valor no és compatible. */ - public static function validateBodyWithClass(array $body, string $class): void + public function validateBodyWithClass(array $body, string $class): void { $reflector = new ReflectionClass($class); foreach ($body as $property => $value) { @@ -55,9 +54,9 @@ public static function validateBodyWithClass(array $body, string $class): void ($value !== null && get_debug_type($value) !== $classProperty->getType()->getName())) && !(get_debug_type($value) == "int" && $classProperty->getType()->getName() == "float") ) { - parent::respostaSimple(400, [ - "error" => "El camp '" . $property . "' no pot ser '" . gettype($value) . "'. Hauria de ser '" . $classProperty->getType() . "'.", - ]); + // parent::respostaSimple(400, [ + // "error" => "El camp '" . $property . "' no pot ser '" . gettype($value) . "'. Hauria de ser '" . $classProperty->getType() . "'.", + // ]); } } } diff --git a/back/models/Accio.php b/src/models/Accio.php similarity index 88% rename from back/models/Accio.php rename to src/models/Accio.php index 39eea1a..df73439 100644 --- a/back/models/Accio.php +++ b/src/models/Accio.php @@ -6,9 +6,9 @@ class Accio { private $dbcnx; - private $table = 'piscinaAccio'; + private $table = 'accio'; - public ?int $accioID; + public ?int $accioId; public ?string $data_hora; public ?int $ph; public ?int $clor; @@ -27,7 +27,7 @@ public function __construct($db) public function read($limit = 20) { - $query = 'SELECT ' . $this->table . '.*, userID, user.usuari AS usuari FROM ' . $this->table . ' JOIN user ON ' . $this->table . '.usuari=userID ORDER BY data_hora DESC LIMIT ' . $limit; + $query = 'SELECT ' . $this->table . '.*, usuariId, user.usuari AS usuari FROM ' . $this->table . ' JOIN user ON ' . $this->table . '.usuari=usuariId ORDER BY data_hora DESC LIMIT ' . $limit; // Prepare statement $stmt = $this->dbcnx->prepare($query); diff --git a/back/models/Control.php b/src/models/Control.php similarity index 67% rename from back/models/Control.php rename to src/models/Control.php index b0da344..d1cbd3b 100644 --- a/back/models/Control.php +++ b/src/models/Control.php @@ -1,30 +1,32 @@ usuari)) { $this->getDadesUsuari(); } + $this->data_hora = $this->data_hora ?? date('Y-m-d H:i:s'); } // MÈTOODES ESTÀTICS CRUD @@ -33,17 +35,17 @@ public function __construct( ? array $data = null) * Desa a la base de dades el control actual. Si no conté ID es crearà un control nou a la base de dades. Si ja conté una ID, s'actualitzarà el control a la base de dades. * @return bool ``true`` si s'ha desat correctament, ``false`` en cas contrari. */ - public function desar() : bool + public function desar(): bool { $arrayControl = get_object_vars($this); - if ($this->controlID === null) { + if ($this->controlId === null) { $arrayControl = $this->estandard($arrayControl); return parent::crear($arrayControl); } - unset($arrayControl['controlID']); + unset($arrayControl['controlId']); unset($arrayControl['usuari']); unset($arrayControl['user']); - return parent::updatePerId($arrayControl, $this->controlID); + return parent::updatePerId($arrayControl, $this->controlId); } // GETTERS /** @@ -52,10 +54,14 @@ public function desar() : bool */ public function getDadesUsuari(): bool { - if ($this->usuari == null) { + if ($this->usuari === null) { + return false; + } + $searchUser = User::trobarPerId($this->usuari); + if (!$searchUser) { return false; } - $this->user = User::trobarPerId($this->usuari); + $this->user = $searchUser; return true; } // ALTRES MÈTODES @@ -66,7 +72,7 @@ public function getDadesUsuari(): bool */ private function estandard(array $data): array { - unset($data['controlID']); + unset($data['controlId']); unset($data['data_hora']); unset($data['user']); return $data; diff --git a/back/models/Model.php b/src/models/Model.php similarity index 95% rename from back/models/Model.php rename to src/models/Model.php index 0998427..a802409 100644 --- a/back/models/Model.php +++ b/src/models/Model.php @@ -1,10 +1,11 @@ $data */ - public function __construct( ? array $data = null) + public function __construct(?array $data = null) { if ($data != null) { foreach ($data as $key => $value) { @@ -39,7 +40,7 @@ public function __construct( ? array $data = null) * Connecta a la base de dades. * @return void */ - private static function connect() : void + private static function connect(): void { $database = new Database(); self::$dbcnx = $database->connect(); @@ -111,7 +112,7 @@ public static function trobarPerId(int $id): static | null | false * @return static[]|null Array d'instàncies que compleixen les condicions. * @throws Exception Si falla alguna cosa. */ - public static function trobarMolts( ? array $condicions = null, int $limit = 20) : ? array + public static function trobarMolts(?array $condicions = null, int $limit = 20): ?array { if (static::$dbcnx === null) { self::connect(); @@ -132,7 +133,9 @@ public static function trobarMolts( ? array $condicions = null, int $limit = 20) } // Si hi ha condicions i no s'especifica límit, el límit passa a ser 1000 (com si no n'hi hagués per mostrar-los tots, però per seguretat limitat) - if (func_num_args() === 1) {$limit = 1000;} + if (func_num_args() === 1) { + $limit = 1000; + } } if ($limit > 0) { $query .= ' LIMIT ' . $limit; @@ -155,7 +158,7 @@ public static function trobarMolts( ? array $condicions = null, int $limit = 20) * @return bool ``true`` si s'ha actualitzat correctament, ``false`` en cas contrari. * @throws InvalidUniqueKey Si no existeix l'identificador únic. */ - private static function updatePerUnic(array $data, string $uniqueKey, $id) : bool + private static function updatePerUnic(array $data, string $uniqueKey, $id): bool { if (!in_array($uniqueKey, static::$uniqueKeyValues)) { throw new InvalidUniqueKey(); diff --git a/back/models/User.php b/src/models/User.php similarity index 74% rename from back/models/User.php rename to src/models/User.php index 2273572..a06df5e 100644 --- a/back/models/User.php +++ b/src/models/User.php @@ -2,21 +2,19 @@ namespace PoolNET; -use PoolNET\config\Env; - class User extends Model { - protected static string $table = 'user'; - protected static string $idKey = 'userID'; - protected static array $uniqueKeyValues = ['userID', 'usuari', 'email']; + protected static string $table = 'usuari'; + protected static string $idKey = 'usuariId'; + protected static array $uniqueKeyValues = ['usuariId', 'usuari', 'email']; // Properties - public int $userID; + public int $usuariId; public string $usuari; protected string $email; protected string $salt; protected string $hash; - public int $nivell; + protected int $nivell; protected string $data_creacio; // MÈTODES ESTÀTICS CRUD @@ -30,6 +28,10 @@ public function getPrivateEmail(): string { return $this->email; } + public function getNivell(): int + { + return $this->nivell; + } // ALTRES MÈTODES /** * Comprova si la contrassenya és vàlida. @@ -38,7 +40,6 @@ public function getPrivateEmail(): string */ public function checkPswd(string $password): bool { - Env::executar(); $hash2 = md5(getenv('ENV_ServerSalt') . $this->salt . $password); return $hash2 === $this->hash; } diff --git a/src/models/errors/Forbidden.php b/src/models/errors/Forbidden.php new file mode 100644 index 0000000..0875841 --- /dev/null +++ b/src/models/errors/Forbidden.php @@ -0,0 +1,13 @@ +Inicia sessió +
+ + + + + + + + + + +
Usuari:
Contrassenya:
Recorda'm a aquest ordinador
+ + +
+ HTML; + $loginPage->addToBody($body); + return $loginPage; +} diff --git a/src/pages/mainPage.php b/src/pages/mainPage.php new file mode 100644 index 0000000..e02ff3f --- /dev/null +++ b/src/pages/mainPage.php @@ -0,0 +1,20 @@ +PISCINA + + EOT; + $mainPage->addToBody($body); + return $mainPage; +} diff --git a/back/phpunit.xml b/src/phpunit.xml similarity index 54% rename from back/phpunit.xml rename to src/phpunit.xml index bccafdc..713e66d 100644 --- a/back/phpunit.xml +++ b/src/phpunit.xml @@ -8,25 +8,41 @@ beStrictAboutCoverageMetadata="true" beStrictAboutOutputDuringTests="true" failOnRisky="false" - failOnWarning="true"> + failOnWarning="true" + testdox="false" + displayDetailsOnTestsThatTriggerDeprecations="true" + displayDetailsOnTestsThatTriggerErrors="true" + displayDetailsOnTestsThatTriggerNotices="true" + displayDetailsOnTestsThatTriggerWarnings="true" +> - api config controllers middlewares models + routes + services + + **/index.php + config/env-example.php + config/database.sql + - + + - - models/JwtHandler.php - __tests__/models + + __tests__ + + + + diff --git a/back/api/accio/get.php b/src/routes/api/accio/get.php similarity index 90% rename from back/api/accio/get.php rename to src/routes/api/accio/get.php index c7f8fe3..32a2409 100644 --- a/back/api/accio/get.php +++ b/src/routes/api/accio/get.php @@ -1,8 +1,7 @@ fetch(PDO::FETCH_ASSOC)) { extract($row); $accio_item = array( - 'id' => $accioID, + 'id' => $accioId, 'data_hora' => $data_hora, 'ph' => $ph, 'clor' => $clor, @@ -34,7 +33,7 @@ 'alcali' => $alcali, 'aglutinant' => $aglutinant, 'usuari' => array( - 'userID' => $userID, + 'usuariId' => $usuariId, 'usuari' => $usuari, ), ); diff --git a/back/api/accio/index.php b/src/routes/api/accio/index.php similarity index 89% rename from back/api/accio/index.php rename to src/routes/api/accio/index.php index df32340..ee9b83d 100644 --- a/back/api/accio/index.php +++ b/src/routes/api/accio/index.php @@ -1,6 +1,6 @@ add($validatorMw); + + $apiRouter = new RouterJson('/api'); + $apiRouter->get("/control", $control); + $apiRouter->post("/auth/login", $authLogin, $authLoginMws); + return $apiRouter; +} diff --git a/back/api/auth/login/index.php b/src/routes/api/auth/login/index.php similarity index 83% rename from back/api/auth/login/index.php rename to src/routes/api/auth/login/index.php index 7f061c1..e35f7db 100644 --- a/back/api/auth/login/index.php +++ b/src/routes/api/auth/login/index.php @@ -1,5 +1,4 @@ "integer"]); + $body = Validator::parseBody(['controlId' => "integer"]); Validator::validateBodyWithClass($body, 'PoolNET\Control'); Control::patch($body); } elseif ($_SERVER['REQUEST_METHOD'] == 'DELETE') { AuthMW::rutaProtegida(); - $body = Validator::parseBody(['controlID' => "integer"]); + $body = Validator::parseBody(['controlId' => "integer"]); Validator::validateBodyWithClass($body, 'PoolNET\Control'); Control::delete($body); } else { diff --git a/src/routes/index.php b/src/routes/index.php new file mode 100644 index 0000000..61443bb --- /dev/null +++ b/src/routes/index.php @@ -0,0 +1,4 @@ +addPage("/", mainPage()); + $router->addPage("/login", loginPage()); + return $router; +} diff --git a/back/models/JwtHandler.php b/src/services/JwtHandler.php similarity index 76% rename from back/models/JwtHandler.php rename to src/services/JwtHandler.php index 81d7f39..7d586fd 100644 --- a/back/models/JwtHandler.php +++ b/src/services/JwtHandler.php @@ -1,26 +1,25 @@ issuedAt = time(); // Token Validity (3600 second = 1hr) $this->expire = $this->issuedAt + 3600; - $this->jwt_secrect = (string) getenv('ENV_JWTSecret'); + $this->jwtSecret = (string) getenv('ENV_JWTSecret'); } /** @@ -39,21 +38,22 @@ public function jwtEncodeData(string $iss, array $data): string "exp" => $this->expire, "data" => $data, ]; - return JWT::encode($token, $this->jwt_secrect, 'HS256'); + return JWT::encode($token, $this->jwtSecret, 'HS256'); } /** * Decodifica un JWT * @param string $jwt_token JWT a decodificar * @return stdClass Les dades codificades al JWT + * @throws InvalidJwtToken */ public function jwtDecodeData(string $jwt_token): stdClass { try { - $decode = JWT::decode($jwt_token, new Key($this->jwt_secrect, 'HS256')); + $decode = JWT::decode($jwt_token, new Key($this->jwtSecret, 'HS256')); return $decode->data; } catch (Exception $e) { - return $e->getMessage(); + throw new InvalidJwtToken(); } } } diff --git a/src/services/MiddlewareArray.php b/src/services/MiddlewareArray.php new file mode 100644 index 0000000..9979a6b --- /dev/null +++ b/src/services/MiddlewareArray.php @@ -0,0 +1,14 @@ +append($middleware); + } +} diff --git a/src/services/Page.php b/src/services/Page.php new file mode 100644 index 0000000..7efbfb3 --- /dev/null +++ b/src/services/Page.php @@ -0,0 +1,102 @@ +title = $title; + $this->session = new Session(); + } + + protected function pageHead(): void + { + echo << + + + + $this->title + + + + + EOT; + } + + protected function pageBody(): void + { + $content = $this->contentBody(); + echo << + $content + + + EOT; + } + + private function contentBody(): string + { + // Check if custom header, body, footer + $this->body .= $this->defaultHeader(); + $this->body .= $this->customBody; + return $this->body; + } + + public function addToBody(string $content): void + { + $this->customBody .= $content; + } + + protected function defaultHeader(): string + { + $content = $this->contentHeader(); + return << + $content + + EOT; + } + + private function contentHeader(): string + { + if ($this->session->loggedInUser) { + return << + + + + + + EOT; + } else { + return << + + + EOT; + } + } + + protected function redirectTo(?string $ruta = null): void + { + header('Location: /' . $ruta); + exit(); + } + + public function render(): void + { + $this->pageHead(); + $this->pageBody(); + exit(); + } +} diff --git a/src/services/Router.php b/src/services/Router.php new file mode 100644 index 0000000..749e88d --- /dev/null +++ b/src/services/Router.php @@ -0,0 +1,70 @@ +prefix = $prefix; + $this->prefixLlarg = $prefix; + $this->format = $format; + $this->routers = new stdClass(); + } + + public function addRouter(string $path, Router $router, Middleware|MiddlewareArray $middleware = null): void + { + $this->routers->$path = $router; + $router->prefixLlarg = $this->removeClosingSlash($this->prefixLlarg) . $router->prefix; + } + + public function use(Request $req, Response $res): bool + { + return $this->useRouter($req, $res); + } + + protected function useRouter(Request $req, Response $res): bool + { + $path = $this->removePrefix($req->getPath()); + foreach ($this->routers as $routerPath => $router) { + if (str_starts_with($path, $routerPath)) { + /** @var Router $router */ + $router->use($req, $res); + return true; + } + } + return false; + } + + protected function removePrefix(string $string): string + { + if (0 === strpos($string, $this->prefixLlarg ?? '')) { + $string = substr($string, strlen($this->prefixLlarg ?? '')); + } + return $this->removeClosingSlash($string); + } + private function removeClosingSlash(string $string): string + { + return strlen($string) > 1 ? rtrim($string, "/") : $string; + } + protected function getSuccessiveRoutes(string $path): array + { + $parts = explode('/', $path); + $currentPath = ''; + $successiveRoutes = []; + foreach ($parts as $part) { + $currentPath .= '/' . $part; + $currentPath = str_replace("//", "/", $currentPath); + $successiveRoutes[] = $currentPath; + } + return $successiveRoutes; + } +} diff --git a/src/services/RouterJson.php b/src/services/RouterJson.php new file mode 100644 index 0000000..76d6242 --- /dev/null +++ b/src/services/RouterJson.php @@ -0,0 +1,104 @@ +controllers = array(); + } + + private function addController(string $path, string $method, Controlador $controlador, ?MiddlewareArray $middlewares = null): void + { + if (!isset($this->controllers[$path])) { + $this->controllers[$path] = array(); + } + $this->controllers[$path][$method] = array("controller" => $controlador, "middlewares" => $middlewares); + } + + public function get(string $path, Get $controlador, ?MiddlewareArray $middlewares = null): void + { + $this->addController($path, "get", $controlador, $middlewares); + } + + public function post(string $path, Post $controlador, ?MiddlewareArray $middlewares = null): void + { + $this->addController($path, "post", $controlador, $middlewares); + } + + public function patch(string $path, Patch $controlador, ?MiddlewareArray $middlewares = null): void + { + $this->addController($path, "patch", $controlador, $middlewares); + } + + public function delete(string $path, Delete $controlador, ?MiddlewareArray $middlewares = null): void + { + $this->addController($path, "delete", $controlador, $middlewares); + } + + public function use(Request $req, Response $res): bool + { + if (!$this->useRouter($req, $res)) { + if (!$this->useController($req, $res)) { + $res->withStatus(404)->toJson(["error" => "Ruta no trobada"]); + return true; + } + return false; + } + return true; + } + protected function useController(Request $req, Response $res): bool + { + $route = $this->removePrefix($req->getPath()); + if (isset($this->controllers[$route])) { + $controller = $this->controllers[$route]; + if (!$this->useMethod($req, $res, $controller)) { + $res->withStatus(405)->toJson(null); + } + return true; + } + return false; + } + protected function useMethod(Request $req, Response $res, array $controllerArray): bool + { + $method = $req->getMethod(); + if (isset($controllerArray[$method])) { + $mwArray = $controllerArray[$method]["middlewares"]; + if ($this->useMw($req, $res, $mwArray)) { + /** @var Controlador $controller */ + $controller = $controllerArray[$method]["controller"]; + try { + $controller->$method($req, $res); + } catch (Throwable $th) { + $res->handleError($th); + } + } + return true; + } + return false; + } + + protected function useMw(Request $req, Response $res, ?MiddlewareArray $mwArray): bool + { + if ($mwArray === null) { + return true; + } + foreach ($mwArray as $mv) { + /** @var Middleware $mw */ + $result = $mv->use($req, $res); + if (!$result) { + return false; + } + } + return true; + } +} diff --git a/src/services/RouterPage.php b/src/services/RouterPage.php new file mode 100644 index 0000000..320cfae --- /dev/null +++ b/src/services/RouterPage.php @@ -0,0 +1,40 @@ +pages = new stdClass(); + } + + public function addPage(string $path, Page $page): void + { + $this->pages->$path = $page; + } + + public function use(Request $req, Response $res): bool + { + $path = $this->removePrefix($req->getPath()); + parent::use($req, $res); + $routes = $this->getSuccessiveRoutes($path); + foreach ($routes as $route) { + if (isset($this->pages->$route)) { + $page = $this->pages->$route; + if ($page instanceof Page) { + $page->render(); + return true; + } + } + } + return false; + } +}