diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..63087a5
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,14 @@
+# Port exposé sur la machine hôte
+APP_PORT=8080
+
+# Compte administrateur créé au premier démarrage
+ADMIN_IDENTIFIANT=admin
+ADMIN_MOT_DE_PASSE=admin123
+ADMIN_EMAIL=glainey@planete-online.fr
+
+# Clé API RAWG (https://rawg.io/apidocs) — optionnelle
+# Sans clé, la saisie manuelle des jeux reste possible
+RAWG_API_KEY=
+
+# Durée de vie de la session en secondes (défaut : 24 h)
+SESSION_LIFETIME=86400
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b893a85
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+.git
+.env
+data/utilisateurs.json
+data/consoles.json
+data/jeux.json
+data/jaquettes/*
+data/cache/*
+!data/jaquettes/.gitkeep
+!data/cache/.gitkeep
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..4669d62
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,21 @@
+FROM php:8.3-apache
+
+RUN a2enmod rewrite headers \
+ && docker-php-ext-install opcache
+
+COPY docker/apache.conf /etc/apache2/sites-available/000-default.conf
+COPY docker/php.ini /usr/local/etc/php/conf.d/jeuxvideotech.ini
+
+WORKDIR /var/www/html
+
+COPY public /var/www/html/public
+COPY src /var/www/html/src
+COPY templates /var/www/html/templates
+COPY data /var/www/html/data
+
+RUN mkdir -p /var/www/html/data/jaquettes /var/www/html/data/cache \
+ && chown -R www-data:www-data /var/www/html/data
+
+ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
+
+EXPOSE 80
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..70747d6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,80 @@
+# JeuxVideoTech
+
+Application web multi-utilisateurs pour gérer une bibliothèque personnelle de **consoles** et de **jeux vidéo**.
+
+Stack : PHP 8.3 + Apache, fichiers JSON, Docker Compose, enrichissement optionnel via l’API [RAWG](https://rawg.io/apidocs).
+
+## Démarrage rapide
+
+```bash
+cp .env.example .env
+docker compose up -d --build
+```
+
+Ouvrir [http://localhost:8080](http://localhost:8080).
+
+Compte administrateur créé automatiquement au premier démarrage (valeurs dans `.env`) :
+
+| Champ | Valeur par défaut |
+|---|---|
+| Identifiant | `admin` |
+| Mot de passe | `admin123` |
+
+**Changez ces valeurs avant tout usage réel.**
+
+## Fonctionnalités
+
+- Inscription / connexion / déconnexion (sessions PHP, CSRF, mots de passe hashés)
+- Isolation des données par compte
+- CRUD consoles et jeux
+- Recherche et filtres sur les jeux (console, état, texte)
+- Enrichissement automatique (jaquette, description, date, genres, éditeur / développeur) si `RAWG_API_KEY` est définie
+- Administration des comptes (rôle `admin`)
+- Persistance via volume Docker (`./data`)
+
+## Configuration
+
+Variables dans `.env` :
+
+| Variable | Description |
+|---|---|
+| `APP_PORT` | Port hôte (défaut `8080`) |
+| `ADMIN_IDENTIFIANT` | Identifiant du compte admin initial |
+| `ADMIN_MOT_DE_PASSE` | Mot de passe du compte admin initial |
+| `ADMIN_EMAIL` | E-mail du compte admin |
+| `RAWG_API_KEY` | Clé API RAWG (optionnelle) |
+| `SESSION_LIFETIME` | Durée de session en secondes |
+
+Sans clé RAWG, l’ajout manuel des jeux reste possible.
+
+## Structure
+
+```
+public/ Point d’entrée Apache + assets
+src/ Bootstrap, auth, services, contrôleurs
+templates/ Vues PHP
+data/ JSON + jaquettes + cache API (volume)
+docker/ Config Apache / PHP
+```
+
+## États de jeu
+
+- `possede` — Possédé
+- `a_acheter` — À acheter
+- `prete` — Prêté
+- `vendu` — Vendu
+
+## Commandes utiles
+
+```bash
+# Lancer
+docker compose up -d --build
+
+# Logs
+docker compose logs -f app
+
+# Arrêter
+docker compose down
+```
+
+Les données (`data/*.json`, jaquettes) survivent au redémarrage du conteneur grâce au volume monté.
diff --git a/data/cache/.gitkeep b/data/cache/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/data/jaquettes/.gitkeep b/data/jaquettes/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..77ca1bc
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,20 @@
+services:
+ app:
+ build: .
+ container_name: jeuxvideotech
+ ports:
+ - "${APP_PORT:-8080}:80"
+ volumes:
+ - ./data:/var/www/html/data
+ - ./public:/var/www/html/public
+ - ./src:/var/www/html/src
+ - ./templates:/var/www/html/templates
+ environment:
+ APP_NAME: "JeuxVideoTech"
+ APP_URL: "http://localhost:${APP_PORT:-8080}"
+ ADMIN_IDENTIFIANT: "${ADMIN_IDENTIFIANT:-admin}"
+ ADMIN_MOT_DE_PASSE: "${ADMIN_MOT_DE_PASSE:-admin123}"
+ ADMIN_EMAIL: "${ADMIN_EMAIL:-glainey@planete-online.fr}"
+ RAWG_API_KEY: "${RAWG_API_KEY:-}"
+ SESSION_LIFETIME: "${SESSION_LIFETIME:-86400}"
+ restart: unless-stopped
diff --git a/docker/apache.conf b/docker/apache.conf
new file mode 100644
index 0000000..4b00b55
--- /dev/null
+++ b/docker/apache.conf
@@ -0,0 +1,12 @@
+
Impossible de contacter l'API pour le moment.
"; + } + finally + { + boutonRecherche.disabled = false; + boutonRecherche.textContent = "Rechercher"; + } + }; + + const afficherResultats = (resultats) => + { + zoneResultats.hidden = false; + + if (resultats.length === 0) + { + zoneResultats.innerHTML = "Aucun résultat trouvé.
"; + return; + } + + zoneResultats.innerHTML = ""; + + resultats.forEach((jeu) => + { + const carte = document.createElement("article"); + carte.className = "api-result"; + + const image = document.createElement("img"); + image.src = jeu.jaquetteUrl || "/assets/img/jaquette-manquante.svg"; + image.alt = "Jaquette de " + (jeu.titre || "jeu"); + + const infos = document.createElement("div"); + const titre = document.createElement("h3"); + titre.textContent = jeu.titre || "Sans titre"; + const meta = document.createElement("p"); + const genres = (jeu.genres || []).join(", "); + meta.textContent = [jeu.dateSortie || "Date inconnue", genres].filter(Boolean).join(" · "); + infos.appendChild(titre); + infos.appendChild(meta); + + const bouton = document.createElement("button"); + bouton.type = "button"; + bouton.className = "btn btn-sm btn-neon"; + bouton.textContent = "Utiliser"; + bouton.addEventListener("click", () => + { + if (!formAppliquer || !champRawgId) + { + return; + } + + champRawgId.value = String(jeu.rawgId); + formAppliquer.submit(); + }); + + carte.appendChild(image); + carte.appendChild(infos); + carte.appendChild(bouton); + zoneResultats.appendChild(carte); + }); + }; + + boutonRecherche.addEventListener("click", lancerRecherche); + + champRecherche.addEventListener("keydown", (event) => + { + if (event.key === "Enter") + { + event.preventDefault(); + lancerRecherche(); + } + }); +}); diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..b547bec --- /dev/null +++ b/public/index.php @@ -0,0 +1,10 @@ +dispatcher( + $_SERVER["REQUEST_METHOD"] ?? "GET", + $_SERVER["REQUEST_URI"] ?? "/" +); diff --git a/public/servir-jaquette.php b/public/servir-jaquette.php new file mode 100644 index 0000000..bee21fc --- /dev/null +++ b/public/servir-jaquette.php @@ -0,0 +1,36 @@ + "image/jpeg", + "jpeg" => "image/jpeg", + "png" => "image/png", + "webp" => "image/webp", + "gif" => "image/gif" +]; + +header("Content-Type: " . ($types[$extension] ?? "application/octet-stream")); +header("Cache-Control: public, max-age=604800"); +readfile($chemin); diff --git a/src/Auth.php b/src/Auth.php new file mode 100644 index 0000000..d26fcf7 --- /dev/null +++ b/src/Auth.php @@ -0,0 +1,135 @@ + $duree, + "path" => "/", + "httponly" => true, + "samesite" => "Lax", + "secure" => false + ]); + + session_start(); + } + + public function connecter(array $utilisateur): void + { + session_regenerate_id(true); + $_SESSION[self::CLE_UTILISATEUR] = $utilisateur["id"]; + } + + public function deconnecter(): void + { + $_SESSION = []; + + if (ini_get("session.use_cookies")) + { + $params = session_get_cookie_params(); + setcookie( + session_name(), + "", + time() - 42000, + $params["path"], + $params["domain"] ?? "", + (bool) $params["secure"], + (bool) $params["httponly"] + ); + } + + session_destroy(); + } + + public function estConnecte(): bool + { + return $this->utilisateurCourant() !== null; + } + + public function utilisateurCourant(): ?array + { + $id = $_SESSION[self::CLE_UTILISATEUR] ?? null; + + if (!is_string($id) || $id === "") + { + return null; + } + + $utilisateur = $this->utilisateurService->trouverParId($id); + + if ($utilisateur === null || !($utilisateur["actif"] ?? false)) + { + return null; + } + + return $utilisateur; + } + + public function exigerConnexion(): array + { + $utilisateur = $this->utilisateurCourant(); + + if ($utilisateur === null) + { + Flash::definir("warning", "Veuillez vous connecter pour continuer."); + Helpers::rediriger("/connexion"); + } + + return $utilisateur; + } + + public function exigerAdmin(): array + { + $utilisateur = $this->exigerConnexion(); + + if (($utilisateur["role"] ?? "") !== "admin") + { + Flash::definir("danger", "Accès réservé aux administrateurs."); + Helpers::rediriger("/"); + } + + return $utilisateur; + } + + public function tenterConnexion(string $identifiant, string $motDePasse): bool + { + $utilisateur = $this->utilisateurService->trouverParIdentifiant($identifiant); + + if ($utilisateur === null) + { + return false; + } + + if (!($utilisateur["actif"] ?? false)) + { + return false; + } + + if (!password_verify($motDePasse, $utilisateur["motDePasseHash"])) + { + return false; + } + + $this->connecter($utilisateur); + + return true; + } +} diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 0000000..7232275 --- /dev/null +++ b/src/Config.php @@ -0,0 +1,41 @@ + self::env("APP_NAME", "JeuxVideoTech"), + "appUrl" => self::env("APP_URL", "http://localhost:8080"), + "adminIdentifiant" => self::env("ADMIN_IDENTIFIANT", "admin"), + "adminMotDePasse" => self::env("ADMIN_MOT_DE_PASSE", "admin123"), + "adminEmail" => self::env("ADMIN_EMAIL", "glainey@planete-online.fr"), + "rawgApiKey" => self::env("RAWG_API_KEY", ""), + "sessionLifetime" => (int) self::env("SESSION_LIFETIME", "86400"), + "dataPath" => dirname(__DIR__) . "/data", + "jaquettesPath" => dirname(__DIR__) . "/data/jaquettes", + "cachePath" => dirname(__DIR__) . "/data/cache" + ]; + } + + public static function get(string $cle, mixed $defaut = null): mixed + { + return self::$valeurs[$cle] ?? $defaut; + } + + private static function env(string $cle, string $defaut = ""): string + { + $valeur = getenv($cle); + + if ($valeur === false || $valeur === "") + { + return $defaut; + } + + return $valeur; + } +} diff --git a/src/Controllers/AccueilController.php b/src/Controllers/AccueilController.php new file mode 100644 index 0000000..b6c5aeb --- /dev/null +++ b/src/Controllers/AccueilController.php @@ -0,0 +1,29 @@ +auth->exigerConnexion(); + $utilisateurId = $utilisateur["id"]; + + View::rendre("accueil", [ + "titrePage" => "Accueil", + "utilisateur" => $utilisateur, + "nombreConsoles" => $this->consoleService->compterPourUtilisateur($utilisateurId), + "nombreJeux" => $this->jeuService->compterPourUtilisateur($utilisateurId), + "derniersJeux" => $this->jeuService->derniersAjouts($utilisateurId), + "consoles" => $this->consoleService->listerPourUtilisateur($utilisateurId) + ]); + } +} diff --git a/src/Controllers/AdminController.php b/src/Controllers/AdminController.php new file mode 100644 index 0000000..d3ec58b --- /dev/null +++ b/src/Controllers/AdminController.php @@ -0,0 +1,76 @@ +auth->exigerAdmin(); + + View::rendre("admin/utilisateurs", [ + "titrePage" => "Administration", + "utilisateur" => $admin, + "utilisateurs" => $this->utilisateurService->tout() + ]); + } + + public function basculerActif(string $id): void + { + $admin = $this->auth->exigerAdmin(); + Csrf::exiger(); + + if ($id === $admin["id"]) + { + Flash::definir("warning", "Vous ne pouvez pas désactiver votre propre compte."); + Helpers::rediriger("/admin/utilisateurs"); + } + + $cible = $this->utilisateurService->trouverParId($id); + + if ($cible === null) + { + Flash::definir("danger", "Utilisateur introuvable."); + Helpers::rediriger("/admin/utilisateurs"); + } + + $nouvelEtat = !($cible["actif"] ?? false); + $this->utilisateurService->definirActif($id, $nouvelEtat); + + Flash::definir( + "success", + $nouvelEtat ? "Compte activé." : "Compte désactivé." + ); + Helpers::rediriger("/admin/utilisateurs"); + } + + public function supprimer(string $id): void + { + $admin = $this->auth->exigerAdmin(); + Csrf::exiger(); + + if ($id === $admin["id"]) + { + Flash::definir("warning", "Vous ne pouvez pas supprimer votre propre compte."); + Helpers::rediriger("/admin/utilisateurs"); + } + + if ($this->utilisateurService->supprimer($id)) + { + Flash::definir("success", "Compte supprimé."); + } + else + { + Flash::definir("danger", "Utilisateur introuvable."); + } + + Helpers::rediriger("/admin/utilisateurs"); + } +} diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php new file mode 100644 index 0000000..80273b1 --- /dev/null +++ b/src/Controllers/AuthController.php @@ -0,0 +1,91 @@ +auth->estConnecte()) + { + Helpers::rediriger("/"); + } + + if (Helpers::estPost()) + { + Csrf::exiger(); + + $identifiant = Helpers::champ("identifiant"); + $motDePasse = Helpers::champ("motDePasse"); + + if ($this->auth->tenterConnexion($identifiant, $motDePasse)) + { + Flash::definir("success", "Bienvenue !"); + Helpers::rediriger("/"); + } + + Flash::definir("danger", "Identifiant ou mot de passe incorrect."); + } + + View::rendre("auth/connexion", [ + "titrePage" => "Connexion", + "utilisateur" => null + ], "layout_auth"); + } + + public function inscription(): void + { + if ($this->auth->estConnecte()) + { + Helpers::rediriger("/"); + } + + if (Helpers::estPost()) + { + Csrf::exiger(); + + try + { + $utilisateur = $this->utilisateurService->creer( + Helpers::champ("identifiant"), + Helpers::champ("email"), + Helpers::champ("motDePasse"), + "utilisateur" + ); + + $this->auth->connecter($utilisateur); + Flash::definir("success", "Compte créé avec succès. Bienvenue !"); + Helpers::rediriger("/"); + } + catch (InvalidArgumentException $exception) + { + Flash::definir("danger", $exception->getMessage()); + } + } + + View::rendre("auth/inscription", [ + "titrePage" => "Inscription", + "utilisateur" => null + ], "layout_auth"); + } + + public function deconnexion(): void + { + if (Helpers::estPost()) + { + Csrf::exiger(); + } + + $this->auth->deconnecter(); + $this->auth->demarrerSession(); + Flash::definir("success", "Vous êtes déconnecté."); + Helpers::rediriger("/connexion"); + } +} diff --git a/src/Controllers/ConsoleController.php b/src/Controllers/ConsoleController.php new file mode 100644 index 0000000..093523b --- /dev/null +++ b/src/Controllers/ConsoleController.php @@ -0,0 +1,137 @@ +auth->exigerConnexion(); + $consoles = $this->consoleService->listerPourUtilisateur($utilisateur["id"]); + + $jeuxParConsole = []; + foreach ($consoles as $console) + { + $jeuxParConsole[$console["id"]] = count( + $this->jeuService->listerParConsole($console["id"], $utilisateur["id"]) + ); + } + + View::rendre("consoles/liste", [ + "titrePage" => "Mes consoles", + "utilisateur" => $utilisateur, + "consoles" => $consoles, + "jeuxParConsole" => $jeuxParConsole + ]); + } + + public function detail(string $id): void + { + $utilisateur = $this->auth->exigerConnexion(); + $console = $this->consoleService->trouverPourUtilisateur($id, $utilisateur["id"]); + + if ($console === null) + { + Flash::definir("danger", "Console introuvable."); + Helpers::rediriger("/consoles"); + } + + View::rendre("consoles/detail", [ + "titrePage" => $console["nom"], + "utilisateur" => $utilisateur, + "console" => $console, + "jeux" => $this->jeuService->listerParConsole($id, $utilisateur["id"]) + ]); + } + + public function creer(): void + { + $utilisateur = $this->auth->exigerConnexion(); + + if (Helpers::estPost()) + { + Csrf::exiger(); + + try + { + $console = $this->consoleService->creer($utilisateur["id"], $_POST); + Flash::definir("success", "Console ajoutée."); + Helpers::rediriger("/consoles/" . $console["id"]); + } + catch (InvalidArgumentException $exception) + { + Flash::definir("danger", $exception->getMessage()); + } + } + + View::rendre("consoles/formulaire", [ + "titrePage" => "Ajouter une console", + "utilisateur" => $utilisateur, + "console" => null, + "action" => "/consoles/creer" + ]); + } + + public function modifier(string $id): void + { + $utilisateur = $this->auth->exigerConnexion(); + $console = $this->consoleService->trouverPourUtilisateur($id, $utilisateur["id"]); + + if ($console === null) + { + Flash::definir("danger", "Console introuvable."); + Helpers::rediriger("/consoles"); + } + + if (Helpers::estPost()) + { + Csrf::exiger(); + + try + { + $console = $this->consoleService->modifier($id, $utilisateur["id"], $_POST); + Flash::definir("success", "Console mise à jour."); + Helpers::rediriger("/consoles/" . $console["id"]); + } + catch (InvalidArgumentException $exception) + { + Flash::definir("danger", $exception->getMessage()); + } + } + + View::rendre("consoles/formulaire", [ + "titrePage" => "Modifier " . $console["nom"], + "utilisateur" => $utilisateur, + "console" => $console, + "action" => "/consoles/" . $id . "/modifier" + ]); + } + + public function supprimer(string $id): void + { + $utilisateur = $this->auth->exigerConnexion(); + Csrf::exiger(); + + $console = $this->consoleService->trouverPourUtilisateur($id, $utilisateur["id"]); + + if ($console === null) + { + Flash::definir("danger", "Console introuvable."); + Helpers::rediriger("/consoles"); + } + + $this->jeuService->supprimerParConsole($id, $utilisateur["id"]); + $this->consoleService->supprimer($id, $utilisateur["id"]); + + Flash::definir("success", "Console et jeux associés supprimés."); + Helpers::rediriger("/consoles"); + } +} diff --git a/src/Controllers/JeuController.php b/src/Controllers/JeuController.php new file mode 100644 index 0000000..da80610 --- /dev/null +++ b/src/Controllers/JeuController.php @@ -0,0 +1,267 @@ +auth->exigerConnexion(); + $recherche = Helpers::champ("q"); + $consoleId = Helpers::champ("console"); + $etat = Helpers::champ("etat"); + + $jeux = $this->jeuService->listerPourUtilisateur( + $utilisateur["id"], + $recherche !== "" ? $recherche : null, + $consoleId !== "" ? $consoleId : null, + $etat !== "" ? $etat : null + ); + + $consoles = $this->consoleService->listerPourUtilisateur($utilisateur["id"]); + $consolesParId = []; + + foreach ($consoles as $console) + { + $consolesParId[$console["id"]] = $console; + } + + View::rendre("jeux/liste", [ + "titrePage" => "Mes jeux", + "utilisateur" => $utilisateur, + "jeux" => $jeux, + "consoles" => $consoles, + "consolesParId" => $consolesParId, + "filtres" => [ + "q" => $recherche, + "console" => $consoleId, + "etat" => $etat + ] + ]); + } + + public function detail(string $id): void + { + $utilisateur = $this->auth->exigerConnexion(); + $jeu = $this->jeuService->trouverPourUtilisateur($id, $utilisateur["id"]); + + if ($jeu === null) + { + Flash::definir("danger", "Jeu introuvable."); + Helpers::rediriger("/jeux"); + } + + $console = $this->consoleService->trouverPourUtilisateur( + $jeu["consoleId"], + $utilisateur["id"] + ); + + View::rendre("jeux/detail", [ + "titrePage" => $jeu["titre"], + "utilisateur" => $utilisateur, + "jeu" => $jeu, + "console" => $console + ]); + } + + public function creer(): void + { + $utilisateur = $this->auth->exigerConnexion(); + $consoles = $this->consoleService->listerPourUtilisateur($utilisateur["id"]); + + if ($consoles === []) + { + Flash::definir("warning", "Ajoutez d'abord une console avant d'enregistrer un jeu."); + Helpers::rediriger("/consoles/creer"); + } + + $prefill = $this->construirePrefillDepuisGet(); + + if (Helpers::estPost()) + { + Csrf::exiger(); + + try + { + $donnees = $this->collecterDonneesFormulaire(); + $jeu = $this->jeuService->creer($utilisateur["id"], $donnees); + Flash::definir("success", "Jeu ajouté à votre bibliothèque."); + Helpers::rediriger("/jeux/" . $jeu["id"]); + } + catch (InvalidArgumentException $exception) + { + Flash::definir("danger", $exception->getMessage()); + $prefill = array_merge($prefill, $_POST); + } + } + + View::rendre("jeux/formulaire", [ + "titrePage" => "Ajouter un jeu", + "utilisateur" => $utilisateur, + "jeu" => $prefill, + "consoles" => $consoles, + "action" => "/jeux/creer", + "apiDisponible" => $this->enrichissementService->estDisponible() + ]); + } + + public function modifier(string $id): void + { + $utilisateur = $this->auth->exigerConnexion(); + $jeu = $this->jeuService->trouverPourUtilisateur($id, $utilisateur["id"]); + + if ($jeu === null) + { + Flash::definir("danger", "Jeu introuvable."); + Helpers::rediriger("/jeux"); + } + + $consoles = $this->consoleService->listerPourUtilisateur($utilisateur["id"]); + + if (Helpers::estPost()) + { + Csrf::exiger(); + + try + { + $donnees = $this->collecterDonneesFormulaire(); + $jeu = $this->jeuService->modifier($id, $utilisateur["id"], $donnees); + Flash::definir("success", "Jeu mis à jour."); + Helpers::rediriger("/jeux/" . $jeu["id"]); + } + catch (InvalidArgumentException $exception) + { + Flash::definir("danger", $exception->getMessage()); + $jeu = array_merge($jeu, $_POST); + } + } + + View::rendre("jeux/formulaire", [ + "titrePage" => "Modifier " . $jeu["titre"], + "utilisateur" => $utilisateur, + "jeu" => $jeu, + "consoles" => $consoles, + "action" => "/jeux/" . $id . "/modifier", + "apiDisponible" => $this->enrichissementService->estDisponible() + ]); + } + + public function supprimer(string $id): void + { + $utilisateur = $this->auth->exigerConnexion(); + Csrf::exiger(); + + if ($this->jeuService->supprimer($id, $utilisateur["id"])) + { + Flash::definir("success", "Jeu supprimé."); + } + else + { + Flash::definir("danger", "Jeu introuvable."); + } + + Helpers::rediriger("/jeux"); + } + + public function rechercherApi(): void + { + $this->auth->exigerConnexion(); + + header("Content-Type: application/json; charset=utf-8"); + + $titre = Helpers::champ("q"); + + if ($titre === "") + { + print json_encode(["resultats" => []]); + return; + } + + $resultats = $this->enrichissementService->rechercher($titre); + print json_encode(["resultats" => $resultats], JSON_UNESCAPED_UNICODE); + } + + public function appliquerEnrichissement(): void + { + $utilisateur = $this->auth->exigerConnexion(); + Csrf::exiger(); + + $rawgId = (int) Helpers::champ("rawgId"); + $retour = Helpers::champ("retour", "/jeux/creer"); + + if ($rawgId <= 0) + { + Flash::definir("danger", "Sélection d'enrichissement invalide."); + Helpers::rediriger($retour); + } + + $detail = $this->enrichissementService->detail($rawgId); + + if ($detail === null) + { + Flash::definir("warning", "Impossible de récupérer les métadonnées. Saisie manuelle possible."); + Helpers::rediriger($retour); + } + + $jaquette = $this->enrichissementService->telechargerJaquette($detail["jaquetteUrl"] ?? null); + + $query = http_build_query([ + "titre" => $detail["titre"] ?? "", + "description" => $detail["description"] ?? "", + "dateSortie" => $detail["dateSortie"] ?? "", + "genres" => implode(", ", $detail["genres"] ?? []), + "editeur" => $detail["editeur"] ?? "", + "developpeur" => $detail["developpeur"] ?? "", + "jaquette" => $jaquette ?? "", + "sourceEnrichissement" => "api" + ]); + + $separateur = str_contains($retour, "?") ? "&" : "?"; + Helpers::rediriger($retour . $separateur . $query); + } + + private function collecterDonneesFormulaire(): array + { + return [ + "titre" => Helpers::champ("titre"), + "consoleId" => Helpers::champ("consoleId"), + "etat" => Helpers::champ("etat", "possede"), + "jaquette" => Helpers::champ("jaquette"), + "description" => Helpers::champ("description"), + "dateSortie" => Helpers::champ("dateSortie"), + "genres" => Helpers::champ("genres"), + "editeur" => Helpers::champ("editeur"), + "developpeur" => Helpers::champ("developpeur"), + "notes" => Helpers::champ("notes"), + "sourceEnrichissement" => Helpers::champ("sourceEnrichissement", "manuel") + ]; + } + + private function construirePrefillDepuisGet(): array + { + $genres = Helpers::champ("genres"); + + return [ + "titre" => Helpers::champ("titre"), + "consoleId" => Helpers::champ("consoleId"), + "etat" => Helpers::champ("etat", "possede"), + "jaquette" => Helpers::champ("jaquette"), + "description" => Helpers::champ("description"), + "dateSortie" => Helpers::champ("dateSortie"), + "genres" => $genres !== "" ? array_map("trim", explode(",", $genres)) : [], + "editeur" => Helpers::champ("editeur"), + "developpeur" => Helpers::champ("developpeur"), + "notes" => Helpers::champ("notes"), + "sourceEnrichissement" => Helpers::champ("sourceEnrichissement", "manuel") + ]; + } +} diff --git a/src/Csrf.php b/src/Csrf.php new file mode 100644 index 0000000..9170c5c --- /dev/null +++ b/src/Csrf.php @@ -0,0 +1,46 @@ +'; + } + + public static function verifier(?string $token): bool + { + if ($token === null || empty($_SESSION[self::CLE_SESSION])) + { + return false; + } + + return hash_equals($_SESSION[self::CLE_SESSION], $token); + } + + public static function exiger(): void + { + $token = $_POST["_csrf"] ?? null; + + if (!self::verifier(is_string($token) ? $token : null)) + { + http_response_code(403); + exit("Requête refusée : jeton CSRF invalide."); + } + } +} diff --git a/src/Flash.php b/src/Flash.php new file mode 100644 index 0000000..593e3f4 --- /dev/null +++ b/src/Flash.php @@ -0,0 +1,29 @@ + $type, + "message" => $message + ]; + } + + public static function consommer(): ?array + { + if (empty($_SESSION[self::CLE])) + { + return null; + } + + $flash = $_SESSION[self::CLE]; + unset($_SESSION[self::CLE]); + + return $flash; + } +} diff --git a/src/Helpers.php b/src/Helpers.php new file mode 100644 index 0000000..eae2c67 --- /dev/null +++ b/src/Helpers.php @@ -0,0 +1,79 @@ + "Possédé", + "a_acheter" => "À acheter", + "prete" => "Prêté", + "vendu" => "Vendu" + ]; + } + + public static function libelleEtat(string $etat): string + { + return self::etatsJeu()[$etat] ?? $etat; + } + + public static function urlJaquette(?string $chemin): string + { + if ($chemin === null || $chemin === "") + { + return "/assets/img/jaquette-manquante.svg"; + } + + if (str_starts_with($chemin, "http://") || str_starts_with($chemin, "https://")) + { + return $chemin; + } + + return "/jaquettes/" . ltrim($chemin, "/"); + } + + public static function tronquer(string $texte, int $longueur = 140): string + { + if (mb_strlen($texte) <= $longueur) + { + return $texte; + } + + return mb_substr($texte, 0, $longueur - 1) . "…"; + } +} diff --git a/src/JsonStore.php b/src/JsonStore.php new file mode 100644 index 0000000..4334c27 --- /dev/null +++ b/src/JsonStore.php @@ -0,0 +1,162 @@ +cheminFichier = $cheminFichier; + $this->assurerFichier(); + } + + public function tout(): array + { + $contenu = file_get_contents($this->cheminFichier); + + if ($contenu === false || trim($contenu) === "") + { + return []; + } + + $donnees = json_decode($contenu, true); + + return is_array($donnees) ? $donnees : []; + } + + public function trouverParId(string $id): ?array + { + foreach ($this->tout() as $element) + { + if (($element["id"] ?? null) === $id) + { + return $element; + } + } + + return null; + } + + public function filtrer(callable $filtre): array + { + return array_values(array_filter($this->tout(), $filtre)); + } + + public function ajouter(array $element): array + { + return $this->avecVerrou(function (array &$donnees) use ($element): array + { + $donnees[] = $element; + + return $element; + }); + } + + public function mettreAJour(string $id, array $nouveauxChamps): ?array + { + return $this->avecVerrou(function (array &$donnees) use ($id, $nouveauxChamps): ?array + { + foreach ($donnees as $index => $element) + { + if (($element["id"] ?? null) !== $id) + { + continue; + } + + $donnees[$index] = array_merge($element, $nouveauxChamps); + + return $donnees[$index]; + } + + return null; + }); + } + + public function supprimer(string $id): bool + { + return $this->avecVerrou(function (array &$donnees) use ($id): bool + { + $tailleInitiale = count($donnees); + $donnees = array_values(array_filter( + $donnees, + static fn (array $element): bool => ($element["id"] ?? null) !== $id + )); + + return count($donnees) !== $tailleInitiale; + }); + } + + public function remplacerTout(array $donnees): void + { + $this->avecVerrou(function (array &$existantes) use ($donnees): null + { + $existantes = $donnees; + + return null; + }); + } + + private function assurerFichier(): void + { + $dossier = dirname($this->cheminFichier); + + if (!is_dir($dossier)) + { + mkdir($dossier, 0775, true); + } + + if (!file_exists($this->cheminFichier)) + { + file_put_contents($this->cheminFichier, "[]" . PHP_EOL, LOCK_EX); + } + } + + private function avecVerrou(callable $callback): mixed + { + $handle = fopen($this->cheminFichier, "c+"); + + if ($handle === false) + { + throw new RuntimeException("Impossible d'ouvrir " . $this->cheminFichier); + } + + try + { + flock($handle, LOCK_EX); + rewind($handle); + $contenu = stream_get_contents($handle); + $donnees = []; + + if ($contenu !== false && trim($contenu) !== "") + { + $decode = json_decode($contenu, true); + $donnees = is_array($decode) ? $decode : []; + } + + $resultat = $callback($donnees); + + $json = json_encode( + $donnees, + JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES + ); + + ftruncate($handle, 0); + rewind($handle); + fwrite($handle, $json . PHP_EOL); + fflush($handle); + + return $resultat; + } + finally + { + flock($handle, LOCK_UN); + fclose($handle); + } + } +} diff --git a/src/Router.php b/src/Router.php new file mode 100644 index 0000000..b9b9478 --- /dev/null +++ b/src/Router.php @@ -0,0 +1,66 @@ +ajouter("GET", $chemin, $action); + } + + public function post(string $chemin, callable $action): void + { + $this->ajouter("POST", $chemin, $action); + } + + public function dispatcher(string $methode, string $uri): void + { + $chemin = parse_url($uri, PHP_URL_PATH) ?: "/"; + $chemin = rtrim($chemin, "/") ?: "/"; + + foreach ($this->routes as $route) + { + if ($route["methode"] !== strtoupper($methode)) + { + continue; + } + + $motif = "@^" . $route["motif"] . "$@"; + + if (!preg_match($motif, $chemin, $captures)) + { + continue; + } + + $params = array_filter( + $captures, + static fn ($cle): bool => !is_int($cle), + ARRAY_FILTER_USE_KEY + ); + + call_user_func_array($route["action"], array_values($params)); + return; + } + + http_response_code(404); + View::rendre("erreurs/404", [ + "titrePage" => "Page introuvable", + "utilisateur" => null + ], "layout_auth"); + } + + private function ajouter(string $methode, string $chemin, callable $action): void + { + $motif = preg_replace("#\{([a-zA-Z_]+)\}#", "(?P<$1>[^/]+)", $chemin); + $motif = str_replace("/", "\/", $motif ?? $chemin); + + $this->routes[] = [ + "methode" => $methode, + "motif" => $motif, + "action" => $action + ]; + } +} diff --git a/src/Services/ConsoleService.php b/src/Services/ConsoleService.php new file mode 100644 index 0000000..95f5bbc --- /dev/null +++ b/src/Services/ConsoleService.php @@ -0,0 +1,133 @@ +store = new JsonStore(Config::get("dataPath") . "/consoles.json"); + } + + public function listerPourUtilisateur(string $utilisateurId): array + { + $consoles = $this->store->filtrer( + static fn (array $console): bool => ($console["utilisateurId"] ?? "") === $utilisateurId + ); + + usort( + $consoles, + static fn (array $a, array $b): int => strcmp($a["nom"] ?? "", $b["nom"] ?? "") + ); + + return $consoles; + } + + public function trouverPourUtilisateur(string $id, string $utilisateurId): ?array + { + $console = $this->store->trouverParId($id); + + if ($console === null || ($console["utilisateurId"] ?? "") !== $utilisateurId) + { + return null; + } + + return $console; + } + + public function creer(string $utilisateurId, array $donnees): array + { + $nom = trim((string) ($donnees["nom"] ?? "")); + $constructeur = trim((string) ($donnees["constructeur"] ?? "")); + + if ($nom === "" || $constructeur === "") + { + throw new InvalidArgumentException("Le nom et le constructeur sont obligatoires."); + } + + $annee = $this->normaliserAnnee($donnees["annee"] ?? null); + $maintenant = Helpers::maintenant(); + + $console = [ + "id" => Helpers::genererId(), + "utilisateurId" => $utilisateurId, + "nom" => $nom, + "constructeur" => $constructeur, + "annee" => $annee, + "notes" => trim((string) ($donnees["notes"] ?? "")), + "createdAt" => $maintenant, + "updatedAt" => $maintenant + ]; + + return $this->store->ajouter($console); + } + + public function modifier(string $id, string $utilisateurId, array $donnees): array + { + $console = $this->trouverPourUtilisateur($id, $utilisateurId); + + if ($console === null) + { + throw new RuntimeException("Console introuvable."); + } + + $nom = trim((string) ($donnees["nom"] ?? "")); + $constructeur = trim((string) ($donnees["constructeur"] ?? "")); + + if ($nom === "" || $constructeur === "") + { + throw new InvalidArgumentException("Le nom et le constructeur sont obligatoires."); + } + + $miseAJour = $this->store->mettreAJour($id, [ + "nom" => $nom, + "constructeur" => $constructeur, + "annee" => $this->normaliserAnnee($donnees["annee"] ?? null), + "notes" => trim((string) ($donnees["notes"] ?? "")), + "updatedAt" => Helpers::maintenant() + ]); + + if ($miseAJour === null) + { + throw new RuntimeException("Échec de la mise à jour."); + } + + return $miseAJour; + } + + public function supprimer(string $id, string $utilisateurId): bool + { + $console = $this->trouverPourUtilisateur($id, $utilisateurId); + + if ($console === null) + { + return false; + } + + return $this->store->supprimer($id); + } + + public function compterPourUtilisateur(string $utilisateurId): int + { + return count($this->listerPourUtilisateur($utilisateurId)); + } + + private function normaliserAnnee(mixed $annee): ?int + { + if ($annee === null || $annee === "") + { + return null; + } + + $valeur = (int) $annee; + + if ($valeur < 1970 || $valeur > (int) date("Y") + 2) + { + return null; + } + + return $valeur; + } +} diff --git a/src/Services/EnrichissementService.php b/src/Services/EnrichissementService.php new file mode 100644 index 0000000..acd1685 --- /dev/null +++ b/src/Services/EnrichissementService.php @@ -0,0 +1,250 @@ +estDisponible()) + { + return []; + } + + $cacheCle = "recherche_" . md5(mb_strtolower($titre)); + $cache = $this->lireCache($cacheCle); + + if ($cache !== null) + { + return array_slice($cache, 0, $limite); + } + + $reponse = $this->appeler("/games", [ + "search" => $titre, + "page_size" => $limite + ]); + + if ($reponse === null || !isset($reponse["results"]) || !is_array($reponse["results"])) + { + return []; + } + + $resultats = []; + + foreach ($reponse["results"] as $jeu) + { + $resultats[] = $this->mapperResultatListe($jeu); + } + + $this->ecrireCache($cacheCle, $resultats); + + return $resultats; + } + + public function detail(int $rawgId): ?array + { + if (!$this->estDisponible()) + { + return null; + } + + $cacheCle = "detail_" . $rawgId; + $cache = $this->lireCache($cacheCle); + + if ($cache !== null) + { + return $cache; + } + + $reponse = $this->appeler("/games/" . $rawgId); + + if ($reponse === null) + { + return null; + } + + $detail = $this->mapperDetail($reponse); + $this->ecrireCache($cacheCle, $detail); + + return $detail; + } + + public function telechargerJaquette(?string $urlImage): ?string + { + if ($urlImage === null || $urlImage === "") + { + return null; + } + + $extension = pathinfo(parse_url($urlImage, PHP_URL_PATH) ?? "", PATHINFO_EXTENSION); + $extension = $extension !== "" ? strtolower($extension) : "jpg"; + + if (!in_array($extension, ["jpg", "jpeg", "png", "webp", "gif"], true)) + { + $extension = "jpg"; + } + + $nomFichier = Helpers::genererId() . "." . $extension; + $cheminLocal = Config::get("jaquettesPath") . "/" . $nomFichier; + + $contexte = stream_context_create([ + "http" => [ + "timeout" => 10, + "header" => "User-Agent: JeuxVideoTech/1.0\r\n" + ] + ]); + + $contenu = @file_get_contents($urlImage, false, $contexte); + + if ($contenu === false) + { + return $urlImage; + } + + if (file_put_contents($cheminLocal, $contenu) === false) + { + return $urlImage; + } + + return $nomFichier; + } + + private function mapperResultatListe(array $jeu): array + { + $genres = []; + + foreach ($jeu["genres"] ?? [] as $genre) + { + if (!empty($genre["name"])) + { + $genres[] = $genre["name"]; + } + } + + return [ + "rawgId" => (int) ($jeu["id"] ?? 0), + "titre" => (string) ($jeu["name"] ?? ""), + "dateSortie" => $jeu["released"] ?? null, + "jaquetteUrl" => $jeu["background_image"] ?? null, + "genres" => $genres, + "note" => $jeu["rating"] ?? null + ]; + } + + private function mapperDetail(array $jeu): array + { + $base = $this->mapperResultatListe($jeu); + + $editeurs = []; + foreach ($jeu["publishers"] ?? [] as $editeur) + { + if (!empty($editeur["name"])) + { + $editeurs[] = $editeur["name"]; + } + } + + $developpeurs = []; + foreach ($jeu["developers"] ?? [] as $developpeur) + { + if (!empty($developpeur["name"])) + { + $developpeurs[] = $developpeur["name"]; + } + } + + $description = (string) ($jeu["description_raw"] ?? ""); + + if ($description === "" && !empty($jeu["description"])) + { + $description = trim(strip_tags((string) $jeu["description"])); + } + + return array_merge($base, [ + "description" => $description, + "editeur" => implode(", ", $editeurs), + "developpeur" => implode(", ", $developpeurs) + ]); + } + + private function appeler(string $chemin, array $params = []): ?array + { + $params["key"] = Config::get("rawgApiKey"); + $url = self::BASE_URL . $chemin . "?" . http_build_query($params); + + $contexte = stream_context_create([ + "http" => [ + "method" => "GET", + "timeout" => 8, + "header" => "User-Agent: JeuxVideoTech/1.0\r\nAccept: application/json\r\n" + ] + ]); + + $reponse = @file_get_contents($url, false, $contexte); + + if ($reponse === false) + { + return null; + } + + $donnees = json_decode($reponse, true); + + return is_array($donnees) ? $donnees : null; + } + + private function lireCache(string $cle): ?array + { + $fichier = Config::get("cachePath") . "/" . $cle . ".json"; + + if (!is_file($fichier)) + { + return null; + } + + if (filemtime($fichier) < time() - 86400) + { + return null; + } + + $contenu = file_get_contents($fichier); + + if ($contenu === false) + { + return null; + } + + $donnees = json_decode($contenu, true); + + return is_array($donnees) ? $donnees : null; + } + + private function ecrireCache(string $cle, array $donnees): void + { + $dossier = Config::get("cachePath"); + + if (!is_dir($dossier)) + { + mkdir($dossier, 0775, true); + } + + $fichier = $dossier . "/" . $cle . ".json"; + file_put_contents( + $fichier, + json_encode($donnees, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + LOCK_EX + ); + } +} diff --git a/src/Services/JeuService.php b/src/Services/JeuService.php new file mode 100644 index 0000000..ba00eb1 --- /dev/null +++ b/src/Services/JeuService.php @@ -0,0 +1,249 @@ +store = new JsonStore(Config::get("dataPath") . "/jeux.json"); + } + + public function listerPourUtilisateur( + string $utilisateurId, + ?string $recherche = null, + ?string $consoleId = null, + ?string $etat = null + ): array + { + $jeux = $this->store->filtrer( + static fn (array $jeu): bool => ($jeu["utilisateurId"] ?? "") === $utilisateurId + ); + + if ($consoleId !== null && $consoleId !== "") + { + $jeux = array_values(array_filter( + $jeux, + static fn (array $jeu): bool => ($jeu["consoleId"] ?? "") === $consoleId + )); + } + + if ($etat !== null && $etat !== "") + { + $jeux = array_values(array_filter( + $jeux, + static fn (array $jeu): bool => ($jeu["etat"] ?? "") === $etat + )); + } + + if ($recherche !== null && $recherche !== "") + { + $terme = mb_strtolower($recherche); + $jeux = array_values(array_filter( + $jeux, + static function (array $jeu) use ($terme): bool + { + $champs = [ + $jeu["titre"] ?? "", + $jeu["description"] ?? "", + $jeu["editeur"] ?? "", + $jeu["developpeur"] ?? "", + implode(" ", $jeu["genres"] ?? []) + ]; + + return str_contains(mb_strtolower(implode(" ", $champs)), $terme); + } + )); + } + + usort( + $jeux, + static fn (array $a, array $b): int => strcmp($a["titre"] ?? "", $b["titre"] ?? "") + ); + + return $jeux; + } + + public function trouverPourUtilisateur(string $id, string $utilisateurId): ?array + { + $jeu = $this->store->trouverParId($id); + + if ($jeu === null || ($jeu["utilisateurId"] ?? "") !== $utilisateurId) + { + return null; + } + + return $jeu; + } + + public function listerParConsole(string $consoleId, string $utilisateurId): array + { + return $this->listerPourUtilisateur($utilisateurId, null, $consoleId, null); + } + + public function derniersAjouts(string $utilisateurId, int $limite = 6): array + { + $jeux = $this->listerPourUtilisateur($utilisateurId); + + usort( + $jeux, + static fn (array $a, array $b): int => strcmp($b["createdAt"] ?? "", $a["createdAt"] ?? "") + ); + + return array_slice($jeux, 0, $limite); + } + + public function creer(string $utilisateurId, array $donnees): array + { + $jeu = $this->preparerDonnees($utilisateurId, $donnees); + $maintenant = Helpers::maintenant(); + + $jeu["id"] = Helpers::genererId(); + $jeu["utilisateurId"] = $utilisateurId; + $jeu["createdAt"] = $maintenant; + $jeu["updatedAt"] = $maintenant; + + return $this->store->ajouter($jeu); + } + + public function modifier(string $id, string $utilisateurId, array $donnees): array + { + $existant = $this->trouverPourUtilisateur($id, $utilisateurId); + + if ($existant === null) + { + throw new RuntimeException("Jeu introuvable."); + } + + $jeu = $this->preparerDonnees($utilisateurId, $donnees, $existant); + $jeu["updatedAt"] = Helpers::maintenant(); + + $miseAJour = $this->store->mettreAJour($id, $jeu); + + if ($miseAJour === null) + { + throw new RuntimeException("Échec de la mise à jour."); + } + + return $miseAJour; + } + + public function supprimer(string $id, string $utilisateurId): bool + { + $jeu = $this->trouverPourUtilisateur($id, $utilisateurId); + + if ($jeu === null) + { + return false; + } + + $this->supprimerJaquetteLocale($jeu["jaquette"] ?? null); + + return $this->store->supprimer($id); + } + + public function compterPourUtilisateur(string $utilisateurId): int + { + return count($this->listerPourUtilisateur($utilisateurId)); + } + + public function supprimerParConsole(string $consoleId, string $utilisateurId): int + { + $jeux = $this->listerParConsole($consoleId, $utilisateurId); + $compteur = 0; + + foreach ($jeux as $jeu) + { + if ($this->supprimer($jeu["id"], $utilisateurId)) + { + $compteur++; + } + } + + return $compteur; + } + + private function preparerDonnees(string $utilisateurId, array $donnees, ?array $existant = null): array + { + $titre = trim((string) ($donnees["titre"] ?? "")); + $consoleId = trim((string) ($donnees["consoleId"] ?? "")); + $etat = trim((string) ($donnees["etat"] ?? "possede")); + + if ($titre === "" || $consoleId === "") + { + throw new InvalidArgumentException("Le titre et la console sont obligatoires."); + } + + if (!array_key_exists($etat, Helpers::etatsJeu())) + { + throw new InvalidArgumentException("État de jeu invalide."); + } + + $genres = $donnees["genres"] ?? []; + + if (is_string($genres)) + { + $genres = array_values(array_filter(array_map("trim", explode(",", $genres)))); + } + + if (!is_array($genres)) + { + $genres = []; + } + + $jaquette = trim((string) ($donnees["jaquette"] ?? ($existant["jaquette"] ?? ""))); + + return [ + "titre" => $titre, + "consoleId" => $consoleId, + "etat" => $etat, + "jaquette" => $jaquette, + "description" => trim((string) ($donnees["description"] ?? "")), + "dateSortie" => $this->normaliserDate($donnees["dateSortie"] ?? null), + "genres" => array_values($genres), + "editeur" => trim((string) ($donnees["editeur"] ?? "")), + "developpeur" => trim((string) ($donnees["developpeur"] ?? "")), + "notes" => trim((string) ($donnees["notes"] ?? "")), + "sourceEnrichissement" => trim((string) ($donnees["sourceEnrichissement"] ?? "manuel")) + ]; + } + + private function normaliserDate(mixed $date): ?string + { + if ($date === null || $date === "") + { + return null; + } + + $date = (string) $date; + + if (!preg_match("/^\d{4}-\d{2}-\d{2}$/", $date)) + { + return null; + } + + return $date; + } + + private function supprimerJaquetteLocale(?string $chemin): void + { + if ($chemin === null || $chemin === "") + { + return; + } + + if (str_starts_with($chemin, "http://") || str_starts_with($chemin, "https://")) + { + return; + } + + $fichier = Config::get("jaquettesPath") . "/" . basename($chemin); + + if (is_file($fichier)) + { + unlink($fichier); + } + } +} diff --git a/src/Services/UtilisateurService.php b/src/Services/UtilisateurService.php new file mode 100644 index 0000000..12dca5c --- /dev/null +++ b/src/Services/UtilisateurService.php @@ -0,0 +1,108 @@ +store = new JsonStore(Config::get("dataPath") . "/utilisateurs.json"); + } + + public function tout(): array + { + $utilisateurs = $this->store->tout(); + usort( + $utilisateurs, + static fn (array $a, array $b): int => strcmp($a["identifiant"] ?? "", $b["identifiant"] ?? "") + ); + + return $utilisateurs; + } + + public function trouverParId(string $id): ?array + { + return $this->store->trouverParId($id); + } + + public function trouverParIdentifiant(string $identifiant): ?array + { + $identifiant = mb_strtolower(trim($identifiant)); + + foreach ($this->store->tout() as $utilisateur) + { + if (mb_strtolower($utilisateur["identifiant"] ?? "") === $identifiant) + { + return $utilisateur; + } + } + + return null; + } + + public function creer(string $identifiant, string $email, string $motDePasse, string $role = "utilisateur"): array + { + $identifiant = trim($identifiant); + $email = trim($email); + + if ($identifiant === "" || $motDePasse === "") + { + throw new InvalidArgumentException("Identifiant et mot de passe obligatoires."); + } + + if ($this->trouverParIdentifiant($identifiant) !== null) + { + throw new InvalidArgumentException("Cet identifiant est déjà utilisé."); + } + + if (strlen($motDePasse) < 6) + { + throw new InvalidArgumentException("Le mot de passe doit contenir au moins 6 caractères."); + } + + $maintenant = Helpers::maintenant(); + + $utilisateur = [ + "id" => Helpers::genererId(), + "identifiant" => $identifiant, + "email" => $email, + "motDePasseHash" => password_hash($motDePasse, PASSWORD_DEFAULT), + "role" => $role === "admin" ? "admin" : "utilisateur", + "actif" => true, + "createdAt" => $maintenant, + "updatedAt" => $maintenant + ]; + + return $this->store->ajouter($utilisateur); + } + + public function definirActif(string $id, bool $actif): ?array + { + return $this->store->mettreAJour($id, [ + "actif" => $actif, + "updatedAt" => Helpers::maintenant() + ]); + } + + public function supprimer(string $id): bool + { + return $this->store->supprimer($id); + } + + public function assurerCompteAdmin(): void + { + if (count($this->store->tout()) > 0) + { + return; + } + + $this->creer( + Config::get("adminIdentifiant"), + Config::get("adminEmail"), + Config::get("adminMotDePasse"), + "admin" + ); + } +} diff --git a/src/View.php b/src/View.php new file mode 100644 index 0000000..9b06ef8 --- /dev/null +++ b/src/View.php @@ -0,0 +1,31 @@ +demarrerSession(); +$utilisateurService->assurerCompteAdmin(); + +$authController = new AuthController($auth, $utilisateurService); +$accueilController = new AccueilController($auth, $consoleService, $jeuService); +$consoleController = new ConsoleController($auth, $consoleService, $jeuService); +$jeuController = new JeuController($auth, $jeuService, $consoleService, $enrichissementService); +$adminController = new AdminController($auth, $utilisateurService); + +$router = new Router(); + +$router->get("/", [$accueilController, "index"]); + +$router->get("/connexion", [$authController, "connexion"]); +$router->post("/connexion", [$authController, "connexion"]); +$router->get("/inscription", [$authController, "inscription"]); +$router->post("/inscription", [$authController, "inscription"]); +$router->post("/deconnexion", [$authController, "deconnexion"]); + +$router->get("/consoles", [$consoleController, "liste"]); +$router->get("/consoles/creer", [$consoleController, "creer"]); +$router->post("/consoles/creer", [$consoleController, "creer"]); +$router->get("/consoles/{id}", [$consoleController, "detail"]); +$router->get("/consoles/{id}/modifier", [$consoleController, "modifier"]); +$router->post("/consoles/{id}/modifier", [$consoleController, "modifier"]); +$router->post("/consoles/{id}/supprimer", [$consoleController, "supprimer"]); + +$router->get("/jeux", [$jeuController, "liste"]); +$router->get("/jeux/creer", [$jeuController, "creer"]); +$router->post("/jeux/creer", [$jeuController, "creer"]); +$router->get("/jeux/api/rechercher", [$jeuController, "rechercherApi"]); +$router->post("/jeux/api/appliquer", [$jeuController, "appliquerEnrichissement"]); +$router->get("/jeux/{id}", [$jeuController, "detail"]); +$router->get("/jeux/{id}/modifier", [$jeuController, "modifier"]); +$router->post("/jeux/{id}/modifier", [$jeuController, "modifier"]); +$router->post("/jeux/{id}/supprimer", [$jeuController, "supprimer"]); + +$router->get("/admin/utilisateurs", [$adminController, "index"]); +$router->post("/admin/utilisateurs/{id}/basculer", [$adminController, "basculerActif"]); +$router->post("/admin/utilisateurs/{id}/supprimer", [$adminController, "supprimer"]); + +return $router; diff --git a/templates/accueil.php b/templates/accueil.php new file mode 100644 index 0000000..f883ed6 --- /dev/null +++ b/templates/accueil.php @@ -0,0 +1,71 @@ +Tableau de bord
+Voici un aperçu de votre bibliothèque.
+Aucun jeu pour le moment. Commencez par ajouter une console, puis un jeu.
+ Ajouter un jeu +Administration
+Activez, désactivez ou supprimez les comptes.
+| Identifiant | +Rôle | +Statut | +Créé le | +Actions | +|
|---|---|---|---|---|---|
| + + + vous + + | ++ | + | + + Actif + + Inactif + + | ++ |
+
+
+
+
+
+ —
+
+
+ |
+
Accédez à votre bibliothèque personnelle.
+ + + ++ Pas encore de compte ? + Créer un compte +
diff --git a/templates/auth/inscription.php b/templates/auth/inscription.php new file mode 100644 index 0000000..5a96473 --- /dev/null +++ b/templates/auth/inscription.php @@ -0,0 +1,53 @@ +Créez votre compte pour gérer vos consoles et jeux.
+ + + ++ Déjà inscrit ? + Se connecter +
diff --git a/templates/consoles/detail.php b/templates/consoles/detail.php new file mode 100644 index 0000000..e0f7310 --- /dev/null +++ b/templates/consoles/detail.php @@ -0,0 +1,49 @@ +·
+ + + + +Aucun jeu associé à cette console.
+Consoles
+ +Inventaire
+console 1 ? "s" : ""; ?> enregistrée 1 ? "s" : ""; ?>.
+Aucune console pour l'instant. Ajoutez votre première machine.
+ Ajouter une console +Aucune description.
+ +Jeux
+ +Recherchez un titre pour préremplir jaquette, description et métadonnées.
+RAWG_API_KEY configurée. La saisie manuelle reste disponible.
+ Bibliothèque
+résultat 1 ? "s" : ""; ?>.
+Aucun jeu ne correspond à vos critères.
+Votre inventaire de consoles et de jeux, centralisé.
+