Skip to content
Snippets Groups Projects
Commit 18888eef authored by alec.schmidt's avatar alec.schmidt
Browse files

Merge remote-tracking branch 'refs/remotes/origin/main'

parents d88dfde6 2ccfbdf7
No related branches found
No related tags found
No related merge requests found
......@@ -4,6 +4,7 @@
### Introduction
#### Le projet
Dans le cadre des cours Architecture Web et Application Web, nous avons du créer un site web permettant à des joueurs de s'inscrire, puis de participer à des parties de jeu contre deux autres joueurs.
Le projet se sépare donc en trois parties distinctes :
......@@ -13,6 +14,26 @@ Le projet se sépare donc en trois parties distinctes :
Cette documentation va se porter sur ces parties du projet, en détaillant les aspects techniques ainsi que des requis pour pouvoir les lancer.
#### Méthodologie
Dans ce projet, nous avons décidé de rester ensemble pour programmer. En effet, l'idée d'être à deux sur l'ensemble du projet présente des avantages qui nous semblaient importants :
- **Compréhension**
Nous avions les deux en tout temps une complète compréhension du projet. Cela nous est arrivé à plusieurs reprises de se rendre compte pendant le développement du Frontend que notre Backend présentait des problèmes et nous avons pu régler rapidement les erreurs.
- **Créativité**
Un mélange d'idées pour une qualité accrue. Nous avons pu, en discutant entre nous, pu faire ressortir diverses idées et garder celles qui nous semblent les meilleurs.
#### Problèmes rencontrés
Cependant notre méthodologie n'était pas vide de problématiques :
- **Temps de développement accru**
En effet, à être à deux sur les mêmes parties et à s'organiser pour être ensemble, nous avons sûrement perdu du temps. On estime qu'environ une à deux semaines supplémentaires nous étaient nécessaires.
- **Manque de préparation**
Comme indiqué dans le chapitre sur l'API REST, nous avions commencé notre système de données d'une mauvaise manière. A revenir en arrière, nous avons fait les choses à la va-vite, et nous avons subi de nombreux problèmes de nommages incohérents entre la base de données, le backend et le frontend.
---
### API REST
......@@ -22,4 +43,262 @@ L'API REST va être celle qui va faire le lien entre la base de données et les
##### Database
Pour ce projet, on avait commencé par garder les données "In Memory". Très vite, on a rencontré des problèmes majeurs, comme la persistence des données, ou simplement les actions sur celles-ci.
\ No newline at end of file
Pour ce projet, on avait commencé par garder les données "In Memory". Très vite, on a rencontré des problèmes majeurs, comme la persistence des données, ou simplement les actions sur celles-ci, qui devenaient très lourdes et complexes. Pour y remédier, nous sommes passés sur une base de données SQLite.
Voici le schéma des données :
![DB Diagram](db_diagram.png)
>Pour simplifier le travail, la table Users possède le mot de passe en clair. Évidemment, dans un contexte réel, le bon sens nous dirait d'y stocker les hashs.
Dans le code, nous avons une classe qui gère toute la DB. Pour ce faire, nous avons créé plusieurs méthodes. Pour résumé, chaque requête à une query faisant la requête SQL ainsi que les divers paramètres nécessaires (nom d'utilisateur, id, etc.)
##### API
Dans le backend, on a décidé d'utiliser le wrapper `promised-sqlite3`. Il fonctionne autour du module `sqlite3` et permet l'usage des promesses pour sqlite3. Cela nous permet de gérer facilement les retours de la base de données, sans avoir à se soucier de la synchrone.
Pour gérer les appels à la base de données, nous avons créé une API possédant diverses routes que voici:
`User - /API/v1/user`<br>
Cette route gère le CRUD des utilisateurs avec respectivement les méthodes post, get, patch et delete (pour les deux dernières, on rajoute l'ID à la route ainsi `/API/v1/user/:id`)
Lorsqu'on s'y connexe, l'API vérifie si l'utilisateur est admin et, dans le post et le patch, elle vérifie également les identifiant.
`Question - /API/v1/question`<br>
De façon similaire à user, le CRUD est géré avec les mêmes méthodes post, get, patch et delete. Par contre, à la différence des utilisateurs, il y a deux get. L'un, de base, retournant toutes les questions et un deuxième auquel on ajoute un ID qui ne retournera que la question souhaitée.
`Answers - /API/v1/answer`<br>
Concernant le CRUD des réponses, la particularité se trouve dans le get. En effet, ce dernier a besoin de l'ID de la question pour pouvoir retourner les réponses de la question souhaitée.
`Category - /API/v1/category` <br>
Les catégories ne possèdent pas d'update car nous n'avons pas considéré cette possibilité comme pertinente. Le reste se passe de façon similaire aux utilisateurs.
`Login - /API/v1/login` <br>
La connexion des utilisateurs ne possède qu'une méthode post de par sa nature. En effet, elle ne peut guère faire plus (car tout ce qui concerne les utilisateurs est dans la route user). Cette route prend dans le body un json type User avec le username et le mot de passe et renvoie le token JWT.
#### Problèmes rencontrés
Au début du projet, nous avions décidé de gérer les données sous forme de liste in-memory. Cette méthode nous a causé bien des problèmes et nous avons donc décidé d'utiliser SQLite, ce qui nous a largement simplifié la tâche.
Nous n'avons malheureusement que peu géré les erreures de la base de données. Là où elles ne sont pas nécéssairement utile au projet, on en perd en propreté de code et on risque plus les comportements indéterminés.
### Frontend
Le frontend utilise Angular avec Tailwindcss.
#### Arborescence
Notre arborescence de projet est comme suit :
```
frontend/src/app
├── admin
│ └── deletion-confirmation
├── gameroom
├── interceptors
├── login-systems
│ ├── login
│ └── signup
├── question-systems
│ ├── create-answer
│ ├── create-category
│ ├── create-question
│ ├── remove-category
│ ├── update-answer
│ └── update-question
├── services
├── Types
├── user-dropdown
└── user-systems
├── create-user
└── update-user
```
**admin**
Notre composant qui gère la page de gestion du site, réservée aux administrateurs.
**gameroom**
Composant qui s'occupe de la logique du jeu (cf. Chapitre sur les Sockets)
**interceptors**
Une espèce de middleman qui va intercepter un appel HTTP. Ici, c'est utilisé pour passer automatiquement le token JWT à tous les appels HTTP.
**login-systems**
Pages de login et de création de compte.
**services**
Nos différents services utilisés par notre projet. Ici, un service d'authentification accessibles à tous, un services lié aux questions et un service lié aux utilisateurs accessibles uniquement par les administrateurs et un service lié aux Sockets.
**Types**
Simple recueil de types de données.
**user-dropdown**
Petite modale à fonction décorative.
**user-systems**
modales de création et mise à jour d'utilisateurs. Celle de création est uniquement accessible par un administrateur, car légèrement différente.
#### Aspects importants
Pour permettre un affichage toujours à jour sans avoir à rafraîchir la page entière, nous utilisons les "Behaviors Subjects". Ils sont utilisables en tant qu'observable, afin de permettre le rafraîchissement du composant parent lors d'un changement de valeurs.
Les modales sont in fine de simples ```<div>``` avec un z-index élevé. Cela simplifie largement le code pour un résultat très satisfaisant.
Le code HTML des modales a été inspiré d'un code trouvé en ligne. Malheureusement, au moment où l'on écrit ce rapport, on ne retrouve plus l'adresse du site WEB hôtant ce code.
Les services sont très utiles pour du code qui n'est pas directement lié avec de l'affichage de composent. Ils agissent comme des singletons. Plutôt qu'avoir toute cette logique dans nos composants, on a décidé de les mettres à part dans des services.
#### Problèmes rencontrés
En raison d'un manque de temps, nous n'avons pas implémenté de système de vérification des codes HTTP. Par exemple, si un utilisateur qui cherche à se connexer se trompe de mot de passe, au lieu d'avoir l'information qu'il a rentré de mauvaises informations, il se fait rediriger vers la page principale, sans être connexé.
C'est selon nous un aspect secondaire du projet, et nous avons préféré nous concentrer sur les aspects primaires.
### Sockets IO
Les sockets servent à ouvrire une connexion entre un système backend et un système frontend. Nous allons donc découper la documentation en deux.
#### Backend
Le backend Socket Server s'occupe de recevoir les communications des divers clients, et de gérer la logique du jeu.
Il y a deux phases :
1. En attente
- Des utilisateurs peuvent s'insrire pour une partie
- À chaque communication, on envoie le nombre d'utilisateurs présents dans une partie
- Les utilisateurs peuvent se désinscrire de la partie
- Sur la déconnexion, si l'utilisateur était inscrit, on le désinscrit
- Lorse que trois utilisateurs sont connexés, on lance la partie
- On récupère dix questions aléatoires
- On informe les joueurs du début de la partie
2. En jeu
- Envoyer une question
- Récupérer la réponse d'un utilisateur
- Envoyer les points des utilisateurs
- Répéter jusqu'à plus de question disponible
- Envoyer le total des points
Pour cela, la librairie Socket.io nous offre une classe Io.Server
```ts
this.on('connection'); // Nous permet d'écouter et d'agir sur une connexion
this.broadcast.emit(); // Nous permet d'envoyer un messages à tous les sockets connectés
```
Sur l'écoute de connexion, nous pouvons également récupérer le socket émitteur, ce qui nous permet d'agir en fonction d'un socket en particulier. On peut y enregistrer des écoutes d'évènements, ce qui nous permet d'y créer toute la logique pour la partie de jeu.
#### Frontend
Ici, nous avons décidé d'utiliser un module Node nommé `ngx-socket-io` qui est techniquement un wrapper autour de `socket.io` créé pour Angular.
**Instanciation**
Il est instancié dans `app.module.ts` comme suit :
On crée une config de socket
```ts
const config: SocketIoConfig = { url: 'http://0.0.0.0:30992', options: { autoConnect: false } };
```
autoConnect est à `false` pour faire en sorte de connecter la socket que lorse qu'on affiche les salles de jeu à l'écran.
Ensuite dans les imports, on ajoute :
```ts
SocketIoModule.forRoot(config)
```
**Usage**
Maintenant, nous pouvons simplement récupérer dans un constructeur
```ts
constructor(
private socket: Socket
)
```
**Service**
Tous les aspects liés au Socket sont condensés dans un service ici humblement nommé `socketService`.
Ce service offre les méthodes suivantes :
```ts
get playerNumber()
get gameStart()
get questionID()
get answers()
get points()
get scoreboard()
joinRoom()
leaveRoom()
connectSocket()
disconnectSocket()
sendAnswer(answer: Answer)
sendMessage(text: string)
```
Tous les get ici retourne des Observables, pour les mêmes raisons qu'énoncées au chapitre précédent.
Les autres méthodes vont communiquer avec le back directement.
A la construction du service, on lui passe en paramètre du socket le token JWT de l'utilisateur et on enregistre tous les évènements d'écoutes sur le socket.
**Usage du service**
Dans le constructeur des composants concernés, on peut y ajouter le service.
```ts
private socket: SocketService
```
Cela lui donne accès à toutes les méthodes décrites plus haut.
**gameroom**
La gameroom a quatre états distincts :
1. Salle non-rejointe
- On peut voir le nombre de joueurs dans la salle
- On peut y rentrer
2. Salle rejointe
- On peut attendre que suffisamment de joueurs se connectent
- On peut discuter avec les joueurs actuellement dans la salle
- On peut quitter la salle
3. Partie en cours
- On affiche la question et les réponses reçues
- On peut sélectionner une réponse
- On peut toujours discuter avec les autre joueurs
4. Partie terminée
- On affiche le tableau des scores
- On peut quitter la salle
**chat**
Un aspect supplémentaire qui n'était pas demandé dans l'énoncé du travail a été ajouté : On peut Chatter !
Lorse qu'on est dans la salle, une fenêtre de chat s'ouvre, et on y voit les messages envoyés avec les pseudos des joueurs.
Son fonctionnement est très simple :
1. Le client emit le message vers le backend
2. Le backend broadcast le message vers tous les clients
3. Tous les client affichent le message
#### Aspects importants
Dans le composant `gameroom`, on affiche à la fin d'une partie les résultats. Ces résultats sont dans un dictionnaire ayant pour clé, le pseudo du joueur, et en valeur son nombre de points. La manière d'afficher un dictionnaire à l'aide d'un *ngFor a été tirée d'une question [StackOverflow](https://stackoverflow.com/questions/66338094/how-do-i-make-ngfor-for-dictionary-in-angular).
#### Problèmes rencontrés
Il y a un bug dans notre site que nous n'avons pas réussi à régler, même avec de l'aide. À la fin d'une partie, le tableau des scores s'affiche correctement et est détruit si l'on quitte la page. Cependant, si l'admin revient, l'affichage affichera toujours le dit tableau.
Pour le chat, on a essayé de donner une couleur différente pour chaque pseudo. Ça s'est malheureusement avéré être un des problèmes les plus difficiles que l'on ait pu voir à l'HEPIA, et nous avons été forcés d'abandonner.
### Procédure d'installation
Prérequis : NodeJS
1. Clonez ce repo
2. Lancez le backend
2.1. Allez dans le dossier API `cd API/`
2.2. Installez les dépendances `npm i`
2.3. Lancez le back `npm run start:dev`
3. Lancez le frontend
3.1. Allez dans le dossier frontend `cd frontend/`
3.2. Installez les dépendances `npm i`
3.3. Lancez le front `npm start`
### Procédure d'usage
La dernière étape doit vous donner l'adresse à laquelle le site web est host. Il suffit de copier-coller ce lien dans le navigateur WEB de votre choix !
<mxfile host="65bd71144e">
<diagram id="YQ82JMaFKs9fSYfRQdZt" name="Page-1">
<mxGraphModel dx="656" dy="450" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="6" value="Users" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;html=1;" vertex="1" parent="1">
<mxGeometry x="80" y="40" width="180" height="130" as="geometry"/>
</mxCell>
<mxCell id="7" value="&lt;b&gt;&lt;u&gt;id&lt;/u&gt;&amp;nbsp; &lt;u&gt;integer&lt;/u&gt; &lt;u&gt;autoincrement&lt;/u&gt;&lt;/b&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="6">
<mxGeometry y="26" width="180" height="26" as="geometry"/>
</mxCell>
<mxCell id="8" value="username text unique" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="6">
<mxGeometry y="52" width="180" height="26" as="geometry"/>
</mxCell>
<mxCell id="9" value="password text" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="6">
<mxGeometry y="78" width="180" height="26" as="geometry"/>
</mxCell>
<mxCell id="12" value="admin boolean" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="6">
<mxGeometry y="104" width="180" height="26" as="geometry"/>
</mxCell>
<mxCell id="38" style="edgeStyle=none;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=0.998;entryDx=0;entryDy=0;entryPerimeter=0;fontColor=#FFFFFF;startArrow=none;startFill=0;endArrow=none;endFill=0;" edge="1" parent="1" source="13" target="30">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="13" value="Questions" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;html=1;" vertex="1" parent="1">
<mxGeometry x="330" y="226" width="200" height="140" as="geometry"/>
</mxCell>
<mxCell id="14" value="&lt;b&gt;&lt;u&gt;id&lt;/u&gt;&amp;nbsp; &lt;u&gt;integer&lt;/u&gt; &lt;u&gt;autoincrement&lt;/u&gt;&lt;/b&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="13">
<mxGeometry y="26" width="200" height="26" as="geometry"/>
</mxCell>
<mxCell id="15" value="question text unique" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="13">
<mxGeometry y="52" width="200" height="26" as="geometry"/>
</mxCell>
<mxCell id="16" value="category text" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="13">
<mxGeometry y="78" width="200" height="26" as="geometry"/>
</mxCell>
<mxCell id="19" value="" style="line;strokeWidth=2;html=1;" vertex="1" parent="13">
<mxGeometry y="104" width="200" height="10" as="geometry"/>
</mxCell>
<mxCell id="18" value="Ref: category&amp;nbsp;&lt;span style=&quot;font-family: sans-serif; font-size: 14px;&quot;&gt;⊆ Categories.title&lt;/span&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="13">
<mxGeometry y="114" width="200" height="26" as="geometry"/>
</mxCell>
<mxCell id="20" value="Answers" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;html=1;" vertex="1" parent="1">
<mxGeometry x="80" y="200" width="200" height="166" as="geometry"/>
</mxCell>
<mxCell id="21" value="&lt;b&gt;&lt;u&gt;id&lt;/u&gt;&amp;nbsp; &lt;u&gt;integer&lt;/u&gt; &lt;u&gt;autoincrement&lt;/u&gt;&lt;/b&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="20">
<mxGeometry y="26" width="200" height="26" as="geometry"/>
</mxCell>
<mxCell id="22" value="text_answer text" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="20">
<mxGeometry y="52" width="200" height="26" as="geometry"/>
</mxCell>
<mxCell id="23" value="id_question integer" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="20">
<mxGeometry y="78" width="200" height="26" as="geometry"/>
</mxCell>
<mxCell id="26" value="correct boolean" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="20">
<mxGeometry y="104" width="200" height="26" as="geometry"/>
</mxCell>
<mxCell id="24" value="" style="line;strokeWidth=2;html=1;" vertex="1" parent="20">
<mxGeometry y="130" width="200" height="10" as="geometry"/>
</mxCell>
<mxCell id="25" value="Ref: id_question&amp;nbsp;&lt;span style=&quot;font-family: sans-serif; font-size: 14px;&quot;&gt;⊆ Questions.id&lt;/span&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="20">
<mxGeometry y="140" width="200" height="26" as="geometry"/>
</mxCell>
<mxCell id="28" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontColor=#FFFFFF;endArrow=none;endFill=0;startArrow=none;startFill=0;" edge="1" parent="1" source="23" target="15">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="29" value="Categories" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;html=1;" vertex="1" parent="1">
<mxGeometry x="340" y="100" width="180" height="52" as="geometry"/>
</mxCell>
<mxCell id="30" value="&lt;b&gt;&lt;u&gt;title&lt;/u&gt;&amp;nbsp;&amp;nbsp;&lt;u&gt;text&lt;/u&gt; &lt;u&gt;unique&lt;/u&gt;&lt;/b&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="29">
<mxGeometry y="26" width="180" height="26" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
\ No newline at end of file
Documentation/db_diagram.png

36.4 KiB

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment