Vous êtes sur la page 1sur 10

Domaine Sciences et Technologies

L2 Informatique, Luminy
WEB : Terminal
Année 2021-22

Les documents non fournis avec l’énoncé, calculatrices, téléphones, ordinateurs, etc. sont interdits

Un réseau social minimaliste

On veut implémenter une version minimale d’une application web de type réseau social avec node-js,
express, sqlite et les templates HTML mustache. Les spécifications retenues sont les suivantes.
1. Modèle de données
(a) Tables
i. Une table d’utilisateurs user avec une clé primaire auto-incrémentée id, et deux colonnes name
et password contenant du texte
ii. Une table d’amis friends avec une clé primaire auto-incrémentée id, et deux colonnes name1
et name2 contenant du texte
iii. Une table de demandes d’amis friend_requests avec une clé primaire auto-incrémentée id,
et deux colonnes requestor (celui qui effectue la demande et requestee (celui qui reçoit la
demande) contenant du texte
(b) Fonction d’accès
i. login(user, password) : retourne la clé id associée à l’utilisateur dans la table user si
l’utilisateur existe et si le mot de passe correspond à celui enregistré ; retourne -1 sinon
ii. user_exists(user) : retourne true si l’utilisateur existe et false sinon
iii. new_user(user, password) : si l’utilisateur existe déjà retourne -1 ; sinon ajoute un utilisa-
teur et retourne la clé id associée dans la table user
iv. get_friends(user_name) : retourne un tableau contenant le nom de tous les amis de l’utili-
sateur user_name
v. get_friend_requests_to(user_name) : retourne un tableau contenant le nom de
tous les utilisateurs demandant actuellement à devenir l’ami de l’utilisateur user_name
get_friend_requests_from(user_name) : retourne un tableau contenant le nom de tous
les utilisateurs à qui l’utilisateur user_name addresse actuellement une demande d’ami
vi. request_friend(requestor, requestee) : retourne -1 si requestee ne correspond pas à
un utilisateur enregistré, -2 si requestor ne correspond pas à un utilisateur enregistré, -3
s’il y a déjà une demande d’ami de requestor à requestee enregistrée, -4 s’il y a déjà une
demande d’ami de requestee à requestor enregistrée et -5 si requestor et requestee sont
déjà amis. Si aucun des cas précédent n’est vérifié, enregistre une demande d’ami dans la table
friend_requests et retourne la clé id associée
vii. request_exists(requestor, requestee) : retourne true s’il y a déjà une demande d’ami
de requestor à requestee enregistrée et false sinon
viii. delete_friend_request(requestor, requestee) : supprime toute demande d’ami de
requestee à requestor et de requestor à requestee de la table friend_requests
ix. accept_friend_request(requestor, requestee) : appelle delete_friend_request et
ajoute un entrée pour requestor et requestee dans la table friends. Ne modifie pas la
table et retourne -1 si une telle entrée existe déjà
x. delete_friend(name1, name2) : supprime tout entrée impliquant name1 et name2 (quel que
soit l’ordre) dans la table friends
2. Vues
(a) Un en-tête header, utilisé sur toutes les pages, avec notamment une barre de navigation. Si
l’utilisateur est connecté à son compte personnel, la barre de navigation affiche un lien vers le
profil de l’utilisateur, un lien pour faire une nouvelle demande d’ami et un lien pour se déconnecter
de son compte. Si l’utilisateur n’est pas connecté, la barre de navigation affiche un lien pour se
connecter à un compte existant et un lien pour créer un nouveau compte
(b) Un bas de page footer, utilisé sur toutes les pages
(c) Une page d’accueil index, affichant un message de bienvenue et une image
(d) Une page de connection login, contenant un formulaire pour se connecter à l’aide de son nom
d’utilisateur et de son mot de passe
(e) Une page de création de compte new_user, affichant un formulaire permettant de choisir un
nom d’utilisateur et un mot de passe et optionnellement un message pour signaler que le nom
d’utilisateur choisi est déjà pris
(f) Une page de connection login, contenant un formulaire pour se connecter à l’aide de son nom
d’utilisateur et de son mot de passe
(g) Une page own_profile pour afficher le profil personnel d’un utilisateur connecté. Elle affiche :
i. Un titre « Profil de » suivi du nom de l’utilisateur et optionnellement d’un message
ii. la liste des amis de l’utilisateur avec pour chaque ami un lien vers son profil personnel et un
bouton permettant de le supprimer de sa liste d’amis
iii. la liste des utilisateurs ayant effectué une demande d’ami à destination de l’utilisateur actuel-
lement connecté, avec pour chacun un lien vers son profil personnel, un bouton pour accepter
la demande et un bouton pour refuser la demande
iv. la liste des utilisateurs à destination desquels l’utilisateur actuellement connecté à fait une
demande d’ami, avec pour chacun un lien vers son profil personnel
(h) Une page friend_profile pour afficher le profil d’un ami de l’utilisateur connecté. Elle affiche :
i. Un titre « Profil de » suivi du nom de l’ami considéré et optionnellement d’un message
ii. la liste des amis de cet ami, avec pour chacun d’entre eux un lien vers son profil personnel
(i) Une page other_profile pour afficher le profil d’un utilisateur qui n’est ni l’utilisateur connecté,
ni un de ses amis. Elle affiche :
i. Un titre « Profil de » suivi du nom de l’utilisateur considéré et optionnellement d’un message
ii. un bouton pour effectuer une demande d’ami à destination de cet utilisateur
(j) Une page no_such_profile affichée lorsque l’utilisateur connecté cherche à consulter le profil d’un
utilisateur non inscrit sur le site. Elle affiche :
i. Un titre « Il n’y a pas d’utilisateur » suivi du nom de l’utilisateur non inscrit et optionnellement
d’un message
(k) Une page de demande d’ami friend_request, contenant un formulaire permettant à l’utilisateur
connecté d’effectuer une nouvelle demande d’ami en fournissant le nom de l’utilisateur à qui la
demande doit être addressée
3. Routes et logique de contrôle
(a) Sur la route GET / on affiche la page d’index
(b) Sur les routes GET /login, GET /new_user on affiche respectivement les pages de connection et
de création de compte
(c) La route POST /login essaye de connecter l’utilisateur à partir de l’identifiant et du mot de passe
fournis et redirige l’utilisateur vers son profil personnel si la connection réussit (c’est à dire si
l’identifiant et le mot de passe correspondent à un utilisateur enregistré sur le site) et vers la route
GET /login sinon
(d) La route POST /new_user essaye de créer un nouveau compte avec l’identifiant et le mot de passe
fournis et connecte et redirige l’utilisateur vers son profil personnel si la création réussit (c’est
à dire si le nom d’utilisateur n’est pas déjà pris) et vers la route GET /new_user sinon, avec le
message « User already exists ».
(e) La route GET /logout déconnecte l’utilisateur et le redirige vers la route GET /
(f) La route paramétrée GET /profile/:user_name n’est accessible qu’aux utilisateurs connectés à
leur compte personnel. Selon l’utilisateur connecté et le paramètre de la route elle mène à la vue
own_profile, friend_profile, other_profile ou no_such_profile
(g) La route GET /request_friend n’est accessible qu’aux utilisateurs connectés à leur compte per-
sonnel. Elle mène à la vue friend_request

2
(h) La route POST /request_friend n’est accessible qu’aux utilisateurs connectés à leur compte
personnel. Elle essaye d’enregistrer une demande d’ami sur la base de l’identité de l’utilisateur
connecté et des données de formulaire fournies et redirige vers le profil de l’utilisateur connecté.
Si l’enregistrement de la demande d’ami échoue parce qu’il existe déjà une demande d’ami dans
la direction opposée à celle demandée, elle enregistre une nouvelle amitié avant de rediriger vers
le profil de l’utilisateur connecté. Si l’enregistrement de la demande d’ami échoue pour d’autre
raisons, un message explicatif sera affiché sur le profil de l’utilisateur connecté après la redirection.
(i) La route POST /accept_request n’est accessible qu’aux utilisateurs connectés à leur compte per-
sonnel. Elle accepte une demande d’ami sur la base de l’identité de l’utilisateur connecté et des
données de formulaire fournies et redirige vers le profil de l’utilisateur connecté
(j) La route POST /reject_request n’est accessible qu’aux utilisateurs connectés à leur compte per-
sonnel. Elle décline une demande d’ami sur la base de l’identité de l’utilisateur connecté et des
données de formulaire fournies et redirige vers le profil de l’utilisateur connecté
(k) La route POST /remove_friend n’est accessible qu’aux utilisateurs connectés à leur compte per-
sonnel. Elle supprime une relation d’amitié sur la base de l’identité de l’utilisateur connecté et des
données de formulaire fournies et redirige vers le profil de l’utilisateur connecté

Questions
1. On considère le fichier create_db.js contenant le code :
1 "use strict";
2 const fs = require('fs');
3 const Sqlite = require('better-sqlite3');
4 let db = new Sqlite('db.sqlite');
5 db.prepare('DROP TABLE IF EXISTS user').run();
6 let stmt = db.prepare('CREATE TABLE user (id INTEGER PRIMARY KEY AUTOINCREMENT, '
7 + 'name TEXT, password TEXT)');
8 stmt.run();
et le fichier model.js contenant le code :
1 "use strict";
2 const Sqlite = require('better-sqlite3');
3 let db = new Sqlite('db.sqlite');
4

5 exports.login = function(user, password) {


6 let stmt = db.prepare('SELECT id FROM user WHERE name = ? AND password = ?')
7 let user_record = stmt.get(user, password);
8 if (user_record === undefined) return -1;
9 return user_record.id;
10 }
11

12 exports.new_user = function(user, password) {


13 let stmt = db.prepare('INSERT INTO user (name, password) VALUES (?, ?)');
14 let result = stmt.run(user, password);
15 return result.lastInsertRowid;
16 }
Écrivez la fonction user_exists et modifiez la fonction new_user afin de respecter les spécifications
demandées

3
Solution :
1 exports.user_exists = function(user) {
2 let user_record = db.prepare('SELECT id FROM user WHERE name = ?').get(user);
3 return Boolean(user_record !== undefined)
4 }
5

6 exports.new_user = function(user, password) {


7 if (exports.user_exists(user)) {
8 return -1;
9 }
10 let stmt = db.prepare('INSERT INTO user (name, password) VALUES (?, ?)');
11 let result = stmt.run(user, password);
12 return result.lastInsertRowid;
13 }
2. On considère le template mustache suivant pour la vue login :
1 {{> header}}
2 <div>
3 <div>
4 <h1>Se connecter</h1>
5 <form action="/login" method="post">
6 <input type="text" name="user" placeholder="Nom d'utilisateur">
7 <input type="password" name="password" placeholder="Mot de passe">
8 <input type="submit" value="Se connecter">
9 </form>
10 </div>
11 </div>
12 {{> footer}}
Écrire un template mustache pour la vue new_user. On affichera le contenu de la variable msg sur la
page (message optionnel)
Solution :
1 {{> header}}
2 <div>
3 <div>
4 <h3>{{msg}}</h3>
5 <h1>Nouvel utilisateur</h1>
6 <form action="/new_user" method="post">
7 <input type="text" name="user" placeholder="Nom d'utilisateur">
8 <input type="password" name="password" placeholder="Mot de passe">
9 <input type="submit" value="Créer l'utilisateur">
10 </form>
11 </div>
12 </div>
13 {{> footer}}
3. On considère le fichier server.js contenant le code suivant :
1 "use strict";
2

3 /*** Server for a minimalist social network ***/


4

5 const express = require('express');


6 const mustache = require('mustache-express');
7 const model = require('./model');
8 const app = express();
9 const bodyParser = require('body-parser');
10 app.use(bodyParser.urlencoded({ extended: false }));
11 const cookieSession = require('cookie-session');
12 app.use(cookieSession({

4
13 secret: '430hjionpkner;kjn3[g:heh',
14 }));
15 app.engine('html', mustache());
16 app.set('view engine', 'html');
17 app.set('views', './views');
18

19 function is_authenticated(req, res, next) {


20 if (req.session.user !== undefined) {
21 return next();
22 }
23 res.status(401).send('Authentication required');
24 }
25

26 function mystery(req, res, next) {


27 if (req.session.user !== undefined) {
28 res.locals.authenticated = true;
29 res.locals.name = req.session.name;
30 }
31 return next();
32 }
33

34 app.use(mystery);
35

36 app.get('/', (req, res) => {


37 res.render('index');
38 });
39

40 app.get('/login', (req, res) => {


41 res.render('login');
42 });
43

44 app.post('/login', (req, res) => {


45 const user = model.login(req.body.user, req.body.password);
46 if (user != -1) {
47 req.session.user = user;
48 req.session.name = req.body.user;
49 res.redirect('/profile/' + req.body.user);
50 } else {
51 res.redirect('/login');
52 }
53 });
Quel est l’utilité de la fonction mystery ?
Solution : Elle permet de rendre accessible depuis les templates mustache les variables
authenticated et name qui indiquent respectivement si l’utilisateur émettant une requête http
au serveur est connecté sur le site et, le cas échéant, son nom d’utilisateur
4. Ajouter le code nécessaire pour implémenter les routes GET /new_user, POST /new_user et
GET /logout au sein du fichier server.js. Pour la route GET /new_user on acceptera un message
optionnel à afficher sur le vue new_user par le biais d’une option (« query string ») passée dans l’URL

5
Solution :
1 app.get('/new_user', (req, res) => {
2 const msg = req.query.msg;
3 res.render('new_user', {msg});
4 });
5

6 app.post('/new_user', (req, res) => {


7 const user = model.new_user(req.body.user, req.body.password);
8 if (user != -1) {
9 req.session.user = user;
10 req.session.name = req.body.user;
11 res.redirect('/profile/' + req.body.user);
12 } else {
13 const msg = 'Identifiant ' + req.body.user + ' déjà pris';
14 const encoded_msg = encodeURIComponent(msg);
15 res.redirect('/new_user?msg=' + encoded_msg);
16 }
17 });
18

19 app.get('/logout', (req, res) => {


20 req.session = null;
21 res.redirect('/');
22 });
5. Écrire le code à ajouter au fichier create_dbs.js pour initialiser les tables friends et
friend_requests
Solution :
1 db.prepare('DROP TABLE IF EXISTS friends').run();
2 db.prepare('CREATE TABLE friends (id INTEGER PRIMARY KEY AUTOINCREMENT, '
3 + 'name1 TEXT, name2 TEXT)').run();
4

5 db.prepare('DROP TABLE IF EXISTS friend_requests').run();


6 db.prepare('CREATE TABLE friend_requests (id INTEGER PRIMARY KEY AUTOINCREMENT, '
7 + 'requestor TEXT, requestee TEXT)').run();
6. Écrire la fonction get_friends à ajouter à model.js
Solution :
1 exports.get_friends = function(user_name) {
2 const stmt = db.prepare('SELECT name1, name2 FROM friends '
3 + 'WHERE name1 = ? OR name2 = ?');
4 let friends = [];
5 for (let pair of stmt.iterate(user_name, user_name)) {
6 if (pair.name1 === user_name) {
7 friends.push(pair.name2);
8 } else {
9 friends.push(pair.name1);
10 }
11 }
12 return friends;
13 }
7. Écrire la fonction request_friend à ajouter à model.js

6
Solution :
1 exports.request_friend = function(requestor, requestee) {
2 if (!exports.user_exists(requestee)) {
3 // no such requestee
4 return -1;
5 }
6 if (!exports.user_exists(requestor)) {
7 // no such requestor
8 return -2;
9 }
10 const stmt = db.prepare('SELECT requestor FROM friend_requests '
11 + 'WHERE requestor = ? AND requestee = ?');
12 const request_record = stmt.get(requestor, requestee);
13 if (request_record !== undefined) {
14 // friend request already pending in required direction
15 return -3;
16 }
17 const request_record2 = stmt.get(requestee, requestor);
18 if (request_record2 !== undefined) {
19 // friend request already pending in reverse direction
20 return -4;
21 }
22 const friends = exports.get_friends(requestor);
23 if (friends.includes(requestee)) {
24 // already friends
25 return -5;
26 }
27 const stmt2 = db.prepare('INSERT INTO friend_requests (requestor, requestee) '
28 + 'VALUES (?, ?)');
29 const result = stmt2.run(requestor, requestee);
30 return result.lastInsertRowid;
31 }
8. Écrire les fonctions delete_friend_request et accept_friend_request à ajouter à model.js
Solution :
1 exports.delete_friend_request = function(requestor, requestee) {
2 const stmt = db.prepare('DELETE from friend_requests '
3 + 'WHERE requestor = ? AND requestee = ?');
4 stmt.run(requestor, requestee);
5 stmt.run(requestee, requestor);
6 }
7

8 exports.accept_friend_request = function(requestor, requestee) {


9 if (!exports.request_exists(requestor, requestee)) {
10 return -1 // no such pending friend request
11 }
12 exports.delete_friend_request(requestor, requestee);
13 const stmt = db.prepare('INSERT INTO friends (name1, name2) VALUES (?, ?)');
14 const result = stmt.run(requestor, requestee);
15 return result.lastInsertRowid;
16 }
9. Écrire le code à ajouter à server.js pour implémenter la route GET /profile/:user_name. On
acceptera un message optionnel à afficher sur le profil par le biais d’une option (« query string »)
passée dans l’URL

7
Solution :
1 app.get('/profile/:user_name', is_authenticated, (req, res) => {
2 const msg = req.query.msg;
3 const target_name = req.params.user_name
4 if (model.user_exists(target_name)) {
5 if (target_name == req.session.name) {
6 const friends = model.get_friends(target_name);
7 const requests_to_me = model.get_friend_requests_to(target_name);
8 const requests_from_me = model.get_friend_requests_from(target_name);
9 res.render('own_profile', {target_name, friends,
10 requests_to_me, requests_from_me, msg});
11 } else if (model.get_friends(req.session.name).includes(target_name)) {
12 const friends = model.get_friends(target_name);
13 res.render('friend_profile', {target_name, friends, msg});
14 } else {
15 res.render('other_profile', {target_name, msg});
16 }
17 } else {
18 res.render('no_such_profile', {target_name, msg});
19 }
20 });
10. Écrire les templates mustache pour les vues own_profile, friend_profile et other_profile

8
Solution : own_profile.html
1 {{> header}}
2

3 <h1>Profil de {{target_name}}</h1>
4

5 <span>{{msg}}</span>
6

7 <div>
8 <h2>Amis</h2>
9 {{#friends}}
10 <div>
11 <a href="{{.}}">{{.}}</a>
12 <form action="/remove_friend" method="post" style="display:inline">
13 <button type="submit" name="ex_friend" value="{{.}}">Supprimer</button>
14 </form>
15 </div>
16 {{/friends}}
17 </div>
18

19 <div>
20 <h2>Ils vous demandent d'être leur ami</h2>
21 {{#requests_to_me}}
22 <div>
23 <a href="{{.}}">{{.}}</a>
24 <form action="/accept_request" method="post" style="display:inline">
25 <button type="submit" name="requestor" value="{{.}}">Accepter</button>
26 </form>
27 <form action="/reject_request" method="post" style="display:inline">
28 <button type="submit" name="requestor" value="{{.}}">Refuser</button>
29 </form>
30 </div>
31 {{/requests_to_me}}
32 </div>
33 <div>
34 <h2>Vous attendez leur réponse</h2>
35 {{#requests_from_me}}
36 <div>
37 <a href="{{.}}">{{.}}</a>
38 </div>
39 {{/requests_from_me}}
40 </div>
41

42 {{> footer}}

9
Solution : friend_profile.html
1 {{> header}}
2

3 <h1>Profil de {{target_name}}</h1>
4

5 <span>{{msg}}</span>
6

7 <div>
8 <h2>Amis</h2>
9 {{#friends}}
10 <div>
11 <a href="{{.}}">{{.}}</a>
12 </div>
13 {{/friends}}
14 </div>
15

16 {{> footer}}
Solution : other_profile.html
1 {{> header}}
2

3 <h1>Profil de {{target_name}}</h1>
4

5 <span>{{msg}}</span>
6

7 <form action="/request_friend" method="post" style="display:inline">


8 <button type="submit" name="requestee"
9 value="{{target_name}}">Ajouter comme ami</button>
10 </form>
11

12 {{> footer}}

10

Vous aimerez peut-être aussi