<file_sep>import { Database } from "../../../lib/Database.js";

const DATABASE = new Database({
	host: "127.0.0.1",
	user: "root",
	database: "groupomania_social_network"
});

export { DATABASE as DatabaseService };<file_sep>import { createGzip } from "zlib";

import { Request } from "../Model/Request.js";
import { Controller } from "../Model/Controller.js";
import { Templating } from "../Model/Templating.js";
import { Session as SessionService } from "../Service/Session.js";

import { User } from "../DataModel/User.js";

class UserController extends Controller
{
	constructor()
	{
		super();
	}

	async signupAction(request, response)
	{
		try
		{
			const POST = request.getRequest();
			SessionService.getSession(request, response);
			const USER = new User();

			USER.setPassword(<PASSWORD>);
			USER.setEmail(POST.email);
			USER.setFirstname(POST.firstname);
			USER.setLastname(POST.lastname);
			USER.setJob(POST.job);
			USER.setAdmin(false);
			await USER.save();

			response.statusCode = 303;
			response.setHeader("Location", "/timeline");
			response.setHeader("Content-Encoding", "gzip");
			const encoder = createGzip();
			encoder.pipe(response);
			encoder.end();
		}
		catch (error)
		{
			console.log(error);
			response.statusCode = 500;
			response.end();
		}
	}

	async loginAction(request, response)
	{
		try
		{
			const POST = request.getRequest();
			const SESSION = SessionService.getSession(request, response);

			const USER = await User.getByEmail(POST.email);

			if (!USER || !USER.isPasswordValid(POST.password))
			{
				response.statusCode = 403;
				response.end();
				return;
			}

			// Ne pas oublier de rentrer la valeur de notre user ID dans la session pour s'en resservir un peu partout
			SESSION.set("userId", USER.getId());

			// On ne renvoie pas de json vu que la requête front vient directement du formulaire et qu'on change de page
			response.statusCode = 303;
			response.setHeader("Location", "/timeline");
			response.setHeader("Content-Encoding", "gzip");
			const encoder = createGzip();
			encoder.pipe(response);
			encoder.end();
		}
		catch (error)
		{
			console.log(error);
			response.statusCode = 500;
			response.end();
		}
	}

	async logoutAction(request, response)
	{
		try
		{
			const SESSION = SessionService.getSession(request, response);
			SESSION.delete("userId");

			response.statusCode = 307;
			response.setHeader("Location", "/");
			response.setHeader("Content-Encoding", "gzip");
			const encoder = createGzip();
			encoder.pipe(response);
			encoder.end();
		}
		catch (error)
		{
			console.log(error);
			response.statusCode = 500;
			response.end();
		}
	}
}

export { UserController };
<file_sep>{{layout: layouts/default.html}}
{{include: partials/title.html}}
<ul>
    
    {{for: let i = 0; i < 10; ++i}}

        {{for: let j = 0; j < 5; ++j}}

            {{if: i % 2 === 0}}

                <li>Even number {{i}}</li>

            {{else if: i % 3 === 0}}

                <li>Can be divided by 3 : {{i}}</li>

            {{else}}

                <li>Other {{i}}</li>

            {{endif}}

        {{endfor}}

    {{endfor}}

</ul><file_sep>import { Hash, randomBytes } from "crypto";
import { DatabaseService } from "../Service/Database.js";
import { WeakRefMap } from "../Service/WeakRefMap.js";

const CACHE = new WeakRefMap();

// DAO
class User
{
	static createFromData(data)
	{
		if (!data.id)
		{
			throw new Error("Invalid data");
		}

		let user = CACHE.get(data.id);

		if (!user)
		{
			user = new this();

			user.id = data.id;
			user.email = data.email;
			user.salt = Buffer.from(data.salt, "hex");
			user.hash = data.hash;
			user.firstname = data.firstname;
			user.lastname = data.lastname;
			user.job = data.job;
			user.admin = data.admin ? true : false;
			user.creationDate = data.creation_date;

			CACHE.set(data.id, user);
		}

		return user;

		/*
		const USER_1 = await User.getById(1);
		USER_1.setJob("Comptable");
		const USER_2 = await User.getById(1);
		console.log(USER_2.getJob()); // "Comptable" aussi
		*/
	}


	static async getAll()
	{
		const RESULTS = await DatabaseService.query(
			`
				SELECT
					*
				FROM
					users
			`
		);

		if (!RESULTS.length)
		{
			return [];
		}

		const USERS = RESULTS.map(
			(user) =>
			{
				return this.createFromData(user);
			}
		);

		return USERS;
	}

	static async getById(id)
	{
		const RESULTS = await DatabaseService.query(
			`
				SELECT
					*
				FROM
					users
				WHERE
					id = :id
				LIMIT
					1
			`,
			{
				id: id
			}
		);

		if (!RESULTS[0])
		{
			return null;
		}

		return this.createFromData(RESULTS[0]);
	}

	static async getByEmail(email)
	{
		const RESULTS = await DatabaseService.query(
			`
				SELECT
					*
				FROM
					users
				WHERE
					email = :email
				LIMIT
					1
			`,
			{
				email: email
			}
		);

		if (!RESULTS[0])
		{
			return null;
		}

		return this.createFromData(RESULTS[0]);
	}

	async save()
	{
		if (this.id)
		{
			await DatabaseService.query(
				`
					UPDATE
						users
					SET
						email = :email,
						salt = :salt,
						hash = :hash,
						firstname = :firstname,
						lastname = :lastname,
						job = :job,
						admin = :admin
					WHERE
						id = :id
				`,
				{
					id: this.id,
					email: this.email,
					salt: this.salt.toString("hex"),
					hash: this.hash,
					firstname: this.firstname,
					lastname: this.lastname,
					job: this.job,
					admin: this.admin
				}
			);
		}
		else
		{
			const RESULT = await DatabaseService.query(
				`
					INSERT INTO
						users (email, salt, hash, firstname, lastname, job, admin)
					VALUES
						(:email, :salt, :hash, :firstname, :lastname, :job, :admin)
				`,
				{
					email: this.email,
					salt: this.salt.toString("hex"),
					hash: this.hash,
					firstname: this.firstname,
					lastname: this.lastname,
					job: this.job,
					admin: this.admin
				}
			);

			this.id = RESULT.insertId;
		}
	}

	getId()
	{
		return this.id;
	}

	getEmail()
	{
		return this.email;
	}

	setEmail(email)
	{
		this.email = email;
	}

	isPasswordValid(password)
	{
		const ENCRYPTOR = new Hash("sha256");
		ENCRYPTOR.update(this.salt.toString("binary") + password);
		const HASH = ENCRYPTOR.digest().toString("hex");
		return this.hash === HASH;
	}

	setPassword(password)
	{
		const SALT = randomBytes(32);
		const ENCRYPTOR = new Hash("sha256");
		ENCRYPTOR.update(SALT.toString("binary") + password);
		const HASH = ENCRYPTOR.digest().toString("hex");

		this.salt = SALT;
		this.hash = HASH;
	}

	getFirstname()
	{
		return this.firstname;
	}

	setFirstname(firstname)
	{
		this.firstname = firstname;
	}

	getLastname()
	{
		return this.lastname;
	}

	setLastname(lastname)
	{
		this.lastname = lastname;
	}

	getJob()
	{
		return this.job;
	}

	setJob(job)
	{
		this.job = job;
	}

	isAdmin()
	{
		return this.admin;
	}

	setAdmin(flag)
	{
		this.admin = flag;
	}

	getCreationDate()
	{
		return this.creationDate;
	}
}

export { User };
<file_sep>class Controller {
    constructor() {
    }
}
export { Controller };
<file_sep>-- phpMyAdmin SQL Dump
-- version 5.0.2
-- https://www.phpmyadmin.net/
--
-- Host: 127.0.0.1:3306
-- Generation Time: Oct 27, 2020 at 07:31 PM
-- Server version: 5.7.31
-- PHP Version: 7.3.21

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;

--
-- Database: `groupomania_social_network`
--

-- --------------------------------------------------------

--
-- Table structure for table `discussions`
--

DROP TABLE IF EXISTS `discussions`;
CREATE TABLE IF NOT EXISTS `discussions` (
  `id` bigint(1) UNSIGNED NOT NULL AUTO_INCREMENT,
  `creation_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `user_id` smallint(1) UNSIGNED NOT NULL,
  `first_message_id` bigint(1) UNSIGNED DEFAULT NULL,
  `title` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4;

--
-- Dumping data for table `discussions`
--

INSERT INTO `discussions` (`id`, `creation_date`, `user_id`, `first_message_id`, `title`) VALUES
(7, '2020-10-26 21:50:34', 3, 6, 'Voici la recette des fameux muffins à la banane que j\'avais amenés jeudi dernier !'),
(8, '2020-10-27 02:54:09', 3, 8, 'Vidéo d\'animaux trop marrants !'),
(9, '2020-10-27 03:03:59', 4, 10, 'Quelle musique mettez-vous pour vous détendre ?');

-- --------------------------------------------------------

--
-- Table structure for table `likes`
--

DROP TABLE IF EXISTS `likes`;
CREATE TABLE IF NOT EXISTS `likes` (
  `id` smallint(1) NOT NULL AUTO_INCREMENT,
  `creation_date` datetime NOT NULL,
  `user_id` smallint(1) NOT NULL,
  `message_id` smallint(1) NOT NULL,
  `value` smallint(1) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- --------------------------------------------------------

--
-- Table structure for table `messages`
--

DROP TABLE IF EXISTS `messages`;
CREATE TABLE IF NOT EXISTS `messages` (
  `id` bigint(1) UNSIGNED NOT NULL AUTO_INCREMENT,
  `creation_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `user_id` smallint(1) UNSIGNED NOT NULL,
  `discussion_id` bigint(1) UNSIGNED DEFAULT NULL,
  `content` varchar(2000) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4;

--
-- Dumping data for table `messages`
--

INSERT INTO `messages` (`id`, `creation_date`, `user_id`, `discussion_id`, `content`) VALUES
(1, '2020-10-24 23:41:43', 3, 3, 'Test 1234'),
(2, '2020-10-24 23:44:47', 3, 4, 'Test 1234'),
(3, '2020-10-25 01:19:38', 3, 5, 'Banane 1234'),
(4, '2020-10-25 19:04:43', 3, 4, 'test 1234 mouton'),
(5, '2020-10-25 20:40:49', 3, 6, 'PonyPonyPonyPonyPonyPonyPonyPonyPonyPony'),
(6, '2020-10-26 21:50:34', 3, 7, 'Ecraser les bananes. Ajouter le sucre et l\'oeuf légèrement battu. Ajouter le beurre fondu, puis les ingrédients secs (farine, levure, bicarbonate). \r\n\r\nPlacer dans des moules à muffin.\r\n\r\nCuire à 190°C pendant 20 min. \r\n\r\nDéguster avec modération ;)'),
(7, '2020-10-27 01:34:00', 3, 7, 'Je peux vous proposer une version à la cerise si vous préférez ! :)'),
(8, '2020-10-27 02:54:09', 3, 8, 'https://www.youtube.com/watch?v=z6EchXyieos'),
(9, '2020-10-27 03:02:51', 4, 7, 'Je confirme qu\'ils sont délicieux !'),
(10, '2020-10-27 03:03:59', 4, 9, 'Voici mon mix préféré !\r\n\r\nhttps://www.youtube.com/watch?v=TDcJJYY5sms&t=5654s'),
(11, '2020-10-27 03:04:10', 4, 8, 'Haha, énorme !'),
(12, '2020-10-27 20:06:29', 3, 8, 'Test');

-- --------------------------------------------------------

--
-- Table structure for table `users`
--

DROP TABLE IF EXISTS `users`;
CREATE TABLE IF NOT EXISTS `users` (
  `id` smallint(1) UNSIGNED NOT NULL AUTO_INCREMENT,
  `creation_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `email` varchar(255) NOT NULL,
  `salt` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
  `hash` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
  `firstname` varchar(255) NOT NULL,
  `lastname` varchar(255) NOT NULL,
  `job` varchar(255) NOT NULL,
  `admin` tinyint(1) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;

--
-- Dumping data for table `users`
--

INSERT INTO `users` (`id`, `creation_date`, `email`, `salt`, `hash`, `firstname`, `lastname`, `job`, `admin`) VALUES
(3, '2020-10-24 22:14:56', '<EMAIL>', 'ee9962125d659e10f6b6f2c74600a4328b32954c174bc8a25b9f74baf50ad855', 'ece0db38889438cbea493cac16994cfe4ec115e2e066006d47cc3465f4d819b5', 'Michael', 'Donati', 'Expert Comptable', 0),
(4, '2020-10-26 17:58:34', '<EMAIL>', 'b6bd66e90ad48a8d5cb12ac2e53815d4df8fdecc1cf704a627393d491bda20d1', '<KEY>', 'Jean', 'Dupont', 'Chef de projet', 0);
COMMIT;

/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
<file_sep>import { parse as parseQuery } from "querystring";
class Request {
    constructor(message) {
        this.pathFragments = [];
        this.query = {};
        this.request = {};
        this.rawBody = "";
        this.body = "";
        this.contentType = "";
        this.boundary = "";
        this.requestedURL = message.url || "";
        this.headers = message.headers;
        if (this.headers['content-type'] !== undefined) {
            this.contentType = this.headers['content-type'];
            if (this.contentType.match(/multipart\/form-data;/) !== null) {
                let splitted_content_type = this.contentType.split("=");
                if (splitted_content_type.length === 2) {
                    this.boundary = `--${splitted_content_type[1]}`;
                }
                else {
                }
                this.contentType = "multipart/form-data";
            }
        }
        let splitted_url = this.requestedURL.split("?");
        this.requestedPath = splitted_url[0];
        if (splitted_url[1] !== undefined) {
            this.query = parseQuery(splitted_url[1]);
        }
        let path_fragments = this.requestedPath.split("/");
        path_fragments = path_fragments.filter((value) => {
            return value.length !== 0;
        });
        this.pathFragments = path_fragments;
    }
    getRawBody() {
        return this.rawBody;
    }
    async setRawBody(value) {
        this.rawBody = value;
        switch (this.getContentType()) {
            case "application/json":
                try {
                    this.body = JSON.parse(this.rawBody);
                }
                catch (e) {
                }
                break;
            case "application/x-www-form-urlencoded":
                this.request = parseQuery(this.rawBody);
                break;
            case "multipart/form-data":
                let parts = this.rawBody.split(this.boundary);
                let files = [];
                parts.forEach(async (part, index) => {
                    if (index !== 0 && index < parts.length - 1) {
                        let name_matches = part.match(/name="([^"]+)";?\s+/i);
                        let name = "";
                        if (name_matches !== null) {
                            name = name_matches[1].trim();
                        }
                        let filename_matches = part.match(/filename="([^\n]+)";?\s+/i);
                        let filename = "";
                        if (filename_matches !== null) {
                            filename = filename_matches[1].trim();
                        }
                        let content_type_matches = part.match(/Content-Type: ([^\s]+)\s+/i);
                        let content_type = "";
                        if (content_type_matches !== null) {
                            content_type = content_type_matches[1].trim();
                        }
                        let splitted_part = part.split("\r\n\r\n");
                        if (filename === "") {
                            this.request[name] = splitted_part[1];
                        }
                        else {
                            files.push({
                                name: filename,
                                mimeType: content_type,
                                content: splitted_part[1]
                            });
                        }
                    }
                });
                break;
            default:
                this.body = this.rawBody;
                break;
        }
    }
    getBody() {
        return this.body;
    }
    getBoundary() {
        return this.boundary;
    }
    getContentType() {
        return this.contentType;
    }
    getHeaders() {
        return this.headers;
    }
    getHeader(name) {
        name = name.toString();
        name = name.toLowerCase();
        let header = this.headers[name];
        if (header !== undefined) {
            return header;
        }
        return null;
    }
    getQuery() {
        return this.query;
    }
    setQuery(query) {
        this.query = query;
    }
    getRequestedPath() {
        return this.requestedPath;
    }
    getPathFragments() {
        return this.pathFragments;
    }
    getRequest() {
        return this.request;
    }
}
export { Request };
<file_sep>import { createGzip } from "zlib";
import { Hash, randomBytes } from "crypto";

import { Controller } from "../Model/Controller.js";
import { Templating } from "../Model/Templating.js";
import { Session as SessionService } from "../Service/Session.js";
import { Authentication as AuthenticationService } from "../Service/Authentication.js";
import { User } from "../DataModel/User.js";
import { Discussion } from "../DataModel/Discussion.js";
import { Message } from "../DataModel/Message.js";

class MessageController extends Controller
{
	constructor()
	{
		super();
	}


	async newAction(request, response)
	{
		try
		{
			const SESSION = SessionService.getSession(request, response);
			const IS_LOGGED = AuthenticationService.checkUser(SESSION, response);
			if (!IS_LOGGED)
			{
				return;
			}
			const TEMPLATING = new Templating();

			const GET = request.getQuery();

			let content = await TEMPLATING.render(`new_message.html`, {id: GET.id});

			response.setHeader("Content-Type", "text/html");
			response.setHeader("Content-Encoding", "gzip");
			const encoder = createGzip();
			encoder.pipe(response);
			encoder.write(content);
			encoder.end();
		}
		catch (error)
		{
			console.log(error);
			response.statusCode = 303;
			response.setHeader("Location", "/");
			response.setHeader("Content-Encoding", "gzip");
			const encoder = createGzip();
			encoder.pipe(response);
			encoder.end();
		}
	}

	async postAction(request, response)
	{
		try
		{
			const SESSION = SessionService.getSession(request, response);

			const POST = request.getRequest();
			const USER_ID = SESSION.get("userId");
			const GET = request.getQuery();

			const NEW_MESSAGE = new Message();

			NEW_MESSAGE.setUserId(USER_ID);
			NEW_MESSAGE.setDiscussionId(GET.id);
			NEW_MESSAGE.setContent(POST.content);
			NEW_MESSAGE.save();

			response.statusCode = 303;
			response.setHeader("Location", `/discussion/${GET.id}`);
			response.setHeader("Content-Encoding", "gzip");
			const encoder = createGzip();
			encoder.pipe(response);
			encoder.end();
		}
		catch (error)
		{
			console.log(error);
			response.statusCode = 303;
			response.setHeader("Location", "/");
			response.setHeader("Content-Encoding", "gzip");
			const encoder = createGzip();
			encoder.pipe(response);
			encoder.end();
		}
	}
}
export { MessageController };
<file_sep>import { createRequire } from "module";

const require = createRequire(import.meta.url);
const mysql = require("mysql");

function escape(field)
{
	if (typeof field === "string")
	{
		return "`" + field.replace(/[^\w]/g, "") + "`";
	}

	if (field instanceof Array)
	{
		return field.map(escape).join(", ");
	}

	throw new Error("Invalid field type");
}

function secure(value)
{
	if (typeof value === "function")
	{
		throw new Error("Function used as value");
	}

	if (value == null)
	{
		return "NULL";
	}

	if (value instanceof Date)
	{
		return value.toISOString().substr(0, 19).replace("T", " ");;
	}

	if (value instanceof Array)
	{
		return value.map((subvalue) => secure(subvalue)).join(", ");
	}

	if (typeof value === "object")
	{
		throw new Error("Object used as value");
	}

	return JSON.stringify(value);
}

class Database
{
	constructor(config)
	{
		this.pool = mysql.createPool(config);
	}

	query(query_string, values, fields)
	{
		return new Promise(
			(accept, reject) =>
			{
				this.pool.getConnection(
					(error, connection) =>
					{
						if (error)
						{
							reject(error);
						}
						else
						{
							query_string = query_string.replace(
								/:(\w+)/g,
								(label, key) =>
								{
									if (fields && fields.hasOwnProperty(key))
									{
										return escape(fields[key]);
									}

									if (values && values.hasOwnProperty(key))
									{
										return secure(values[key]);
									}

									return label;
								}
							);

							connection.query(
								query_string,
								(error, results) =>
								{
									connection.release();

									if (error)
									{
										reject(error);
									}
									else
									{
										accept(results);
									}
								}
							);
						}
					}
				);
			}
		);
	}
};

export { Database };
<file_sep>import { promises as FileSystem } from "fs";
class Routing {
    constructor() {
        this.routes = [];
        this.sourceFilePath = `./private/Resources/configuration/routing.json`;
    }
    async loadRoutingFile(path) {
        if (path === undefined) {
            path = this.sourceFilePath;
        }
        try {
            const FILE_STATS = await FileSystem.stat(path);
            if (!FILE_STATS.isFile()) {
                throw new Error("Provided path for routing configuration file does not link to a file.");
            }
            const RAW_CONFIGURATION = await FileSystem.readFile(path, { encoding: "UTF-8" });
            if (RAW_CONFIGURATION instanceof Buffer) {
                throw new Error("TODO: Handle the case where a file returns a Buffer and not a string in Routing.ts.");
            }
            const PARSED_CONFIGURATION = JSON.parse(RAW_CONFIGURATION);
            PARSED_CONFIGURATION.forEach((configuration) => {
                this.routes.push({
                    name: configuration.name,
                    regexp: new RegExp(configuration.regexp),
                    pretty: configuration.pretty,
                    controller: configuration.controller,
                    action: configuration.action,
                    variables: configuration.variables
                });
            });
        }
        catch (e) {
            console.log(e);
        }
    }
    getRoutes() {
        return this.routes;
    }
}
export { Routing };
<file_sep>import { DatabaseService } from "../Service/Database.js";
import { Message } from "./Message.js";
import { User } from "./User.js";
import { WeakRefMap } from "../Service/WeakRefMap.js";

const CACHE = new WeakRefMap();

// DAO
class Discussion
{
	static createFromData(data)
	{
		if (!data.id)
		{
			throw new Error("Invalid data");
		}

		let discussion = CACHE.get(data.id);

		if (!discussion)
		{
			discussion = new this();

			discussion.id = data.id;
			discussion.creationDate = data.creation_date;
			discussion.userId = data.user_id;
			discussion.firstMessageId = data.first_message_id;
			discussion.title = data.title;

			//potential upgrade here
		}

		return discussion;
	}

	static async getById(id)
	{
		const RESULTS = await DatabaseService.query(
			`
				SELECT
					*
				FROM
					discussions
				WHERE
					id = :id
				LIMIT
					1
			`,
			{
				id: id
			}
		);

		if (!RESULTS[0])
		{
			return null;
		}

		return this.createFromData(RESULTS[0]);
	}

	async save()
	{
		if (this.id)
		{
			await DatabaseService.query(
				`
					UPDATE
						discussions
					SET
						user_id = :user_id,
						first_message_id = :first_message_id,
						title = :title
					WHERE
						id = :id
				`,
				{
					id : this.id,
					user_id : this.userId,
					first_message_id : this.firstMessageId,
					title : this.title
				}
			);
		}
		else
		{
			const RESULT = await DatabaseService.query(
				`
					INSERT INTO
						discussions (user_id, first_message_id, title)
					VALUES
						(:user_id, :first_message_id, :title)
				`,
				{
					user_id : this.userId,
					first_message_id : this.firstMessageId,
					title : this.title
				}
			);

			this.id = RESULT.insertId;
		}
	}

	getId()
	{
		return this.id;
	}

	getCreationDate()
	{
		return this.creationDate;
	}

	getUserId()
	{
		return this.userId;
	}

	setUserId(userId)
	{
		this.userId = userId;
	}

	async getUser()
	{
		const USER = await User.getById(this.userId);
		return USER;
	}

	setUser(user)
	{
		if (!user.id)
		{
			throw new Error("User is not saved");
		}

		this.userId = user.id;
	}

	getFirstMessageId()
	{
		return this.firstMessageId;
	}

	setFirstMessageId(firstMessageId)
	{
		this.firstMessageId = firstMessageId;
	}

	async getFirstMessage()
	{
		const MESSAGE = await Message.getById(this.firstMessageId);
		return MESSAGE;
	}

	setFirstMessage(message)
	{
		if (!message.id)
		{
			throw new Error("Message is not saved");
		}

		this.firstMessageId = message.id;
	}

	getTitle()
	{
		return this.title;
	}

	setTitle(title)
	{
		this.title = title;
	}

	async getMessages(sortFunction)
	{
		const RESULTS = await DatabaseService.query(
			`
			SELECT
				*
			FROM
				messages
			WHERE
				discussion_id = :id
			ORDER BY
				id ASC
			`,
			{
				id: this.id
			}
		);

		if (!RESULTS.length)
		{
			return [];
		}

		const MESSAGES = RESULTS.map(
			(data) =>
			{
				return Message.createFromData(data);
			}
		);

		if (sortFunction)
		{
			const FIRST_MESSAGE = MESSAGES.shift();
			MESSAGES.sort(sortFunction);
			MESSAGES.unshift(FIRST_MESSAGE);
		}

		return MESSAGES;
	}

	static async getAll()
	{
		const RESULTS = await DatabaseService.query(
			`
				SELECT
					*
				FROM
					discussions
				ORDER BY
					id DESC
				LIMIT
					50
			`
		);

		if (!RESULTS.length)
		{
			return null;
		}

		const DISCUSSIONS = RESULTS.map(
			(discussion) =>
			{
				return this.createFromData(discussion);
			}
		);

		return DISCUSSIONS;
	}
}

export { Discussion };
<file_sep>import { promises as FileSystem } from "fs";
import { basename } from "path";
class Templating {
    constructor() {
        this.publicDirectory = "";
        const __DIRNAME__ = import.meta.url.replace(/^file:\/\/\/[A-Z]\:(.*)\/[^\/]+$/, "$1");
        this.publicDirectory = `${__DIRNAME__}/../../../www`;
    }
    async render(path, parameters) {
        const __DIRNAME__ = import.meta.url.replace(/^file:\/\/\/[A-Z]\:(.*)\/[^\/]+$/, "$1");
        try {
            const FULL_PATH = `${this.publicDirectory}/${path}`;
            const FILE_STATS = await FileSystem.stat(FULL_PATH);
            if (!FILE_STATS.isFile()) {
                throw new Error(`Attempted to render path "${FULL_PATH}". Directory found.`);
            }
            const FILE = await FileSystem.readFile(FULL_PATH);
            let template = FILE.toString();
            if (parameters !== undefined) {
                const PARAMETERS_NAMES = Object.keys(parameters);
                for (let parameter of PARAMETERS_NAMES) {
                    const REGEXP = new RegExp(`{{${parameter}}}`);
                    template = template.replace(REGEXP, `\${parameter}`);
                }
            }
            const LAYOUT = template.match(/{{layout: *([^}]+)}}/);
            let layout = "";
            if (LAYOUT !== null) {
                layout = LAYOUT[1];
            }
            template = template.replace(/{{layout: *([^}]+)}}/, "");
            template = template.replace(/{{include: *([^}]+)}}/g, "`;\r\nawait this.include('$1');\r\nthis.content += `");
            template = template.replace(/{{const: *([^}]+)}}/g, "`;\r\nconst $1;\r\nthis.content += `");
            template = template.replace(/{{let: *([^}]+)}}/g, "`;\r\nlet $1;\r\nthis.content += `");
            template = template.replace(/{{for: *([^}]+)}}/g, "`;\r\nfor ($1)\r\n{\r\nthis.content += `");
            template = template.replace(/{{endfor}}/g, "`;\r\n}\r\nthis.content += `");
            template = template.replace(/{{if: *([^}]+)}}/g, "`;\r\nif ($1)\r\n{\r\nthis.content += `");
            template = template.replace(/{{else if: *([^}]+)}}/g, "`;\r\n}\r\nelse if ($1) {\r\nthis.content += `");
            template = template.replace(/{{else}}/g, "`;\r\n}\r\nelse\r\n{\r\nthis.content += `");
            template = template.replace(/{{endif}}/g, "`;\r\n}\r\nthis.content += `");
            template = template.replace(/{{([^}]+)}}/g, "${$1}");
            template = `import { View as AbstractView } from "${__DIRNAME__}/View.js";
class View extends AbstractView
{
    constructor(parameters)
    {
        super(parameters);
        this.layout = '${layout}';
    }
    async render()
    {
        this.content = \`${template}\`;
        return this.content;
    }
}
export { View };`;
            const FILENAME = basename(path);
            const SAVE_PATH = path.replace(new RegExp(`/?${FILENAME}`), "");
            const DESTINATION_DIRECTORY = `${__DIRNAME__}/../../cache/${SAVE_PATH}`;
            await FileSystem.mkdir(DESTINATION_DIRECTORY, { recursive: true });
            const FULL_FILE_PATH = `${DESTINATION_DIRECTORY}/${FILENAME}.mjs`;
            await FileSystem.writeFile(FULL_FILE_PATH, template);
            const { View } = await import(FULL_FILE_PATH);
            const VIEW = new View(parameters);
            await VIEW.render();
            await VIEW.build();
            const CONTENT = VIEW.getContent();
            return CONTENT;
        }
        catch (e) {
            console.log("------------------------------------------");
            console.log(e);
            console.log(`Attempted to render path "${path}". File not found.`);
            console.log("------------------------------------------");
            return null;
        }
    }
}
export { Templating };
<file_sep>import { DatabaseService } from "../Service/Database.js";
import { Discussion } from "./Discussion.js";
import { User } from "./User.js";
import { WeakRefMap } from "../Service/WeakRefMap.js";

const CACHE = new WeakRefMap();

// DAO
class Message
{
	static createFromData(data)
	{
		if (!data.id)
		{
			throw new Error("Invalid data");
		}

		let message = CACHE.get(data.id);

		if (!message)
		{
			message = new this();

			message.id = data.id;
			message.creationDate = data.creation_date;
			message.userId = data.user_id;
			message.discussionId = data.discussion_id;
			message.content = data.content;
		}

		return message;
	}

	static async getById(id)
	{
		const RESULTS = await DatabaseService.query(
			`
				SELECT
					*
				FROM
					messages
				WHERE
					id = :id
				LIMIT
					1
			`,
			{
				id: id
			}
		);

		if (!RESULTS[0])
		{
			return null;
		}

		return this.createFromData(RESULTS[0]);
	}

	async getDiscussion()
	{
		const DISCUSSION = await Discussion.getById(this.discussionId);
		return DISCUSSION;
	}

	async save()
	{
		if (this.id)
		{
			await DatabaseService.query(
				`
					UPDATE
						messages
					SET
						user_id = :user_id,
						discussion_id = :discussion_id,
						content = :content
					WHERE
						id = :id
				`,
				{
					id : this.id,
					user_id : this.userId,
					discussion_id : this.discussionId,
					content : this.content
				}
			);
		}
		else
		{
			const RESULT = await DatabaseService.query(
				`
					INSERT INTO
						messages (user_id, discussion_id, content)
					VALUES
						(:user_id, :discussion_id, :content)
				`,
				{
					user_id : this.userId,
					discussion_id : this.discussionId,
					content : this.content
				}
			);

			this.id = RESULT.insertId;
		}
	}

	getId()
	{
		return this.id;
	}

	getCreationDate()
	{
		return this.creationDate;
	}

	getUserId()
	{
		return this.userId;
	}

	setUserId(userId)
	{
		this.userId = userId;
	}

	async getUser()
	{
		const USER = await User.getById(this.userId);
		return USER;
	}

	setUser(user)
	{
		if (!user.id)
		{
			throw new Error("User is not saved");
		}

		this.userId = user.id;
	}

	getDiscussionId()
	{
		return this.discussionId;
	}

	setDiscussionId(discussionId)
	{
		this.discussionId = discussionId;
	}

	async getDiscussion()
	{
		const DISCUSSION = await Discussion.getById(this.discussionId);
		return DISCUSSION;
	}

	setDiscussion(discussion)
	{
		if (!discussion.id)
		{
			throw new Error("Discussion is not saved");
		}

		this.discussionId = discussion.id;
	}

	getContent()
	{
		return this.content;
	}

	setContent(content)
	{
		this.content = content;
	}
}

export { Message };
<file_sep>import { Server } from "./Model/Server.js";
import { promises as Reader } from "fs";
async function initialize() {
    const __DIRNAME__ = import.meta.url.replace(/^file:\/\/\/[A-Z]\:(.*)\/[^\/]+$/, "$1");
    console.log(__DIRNAME__);
    console.log("----------------");
    let key = await Reader.readFile(`${__DIRNAME__}/../../private/Resources/privateKey.key`);
    let cert = await Reader.readFile(`${__DIRNAME__}/../../private/Resources/certificate.crt`);
    let options = {
        key: key,
        cert: cert
    };
    const SERVER = new Server(options, undefined);
    SERVER.addListener("request", SERVER.handleRequest);
    SERVER.start();
}
initialize();
<file_sep>import { createGzip } from "zlib";

import { Controller } from "../Model/Controller.js";
import { Templating } from "../Model/Templating.js";
import { Session as SessionService } from "../Service/Session.js";

import { Discussion } from "../DataModel/Discussion.js";
import { User } from "../DataModel/User.js";

class TimelineController extends Controller
{
	constructor()
	{
        super();
	}

	async timelineAction(request, response)
	{
		SessionService.getSession(request, response);
		const TEMPLATING = new Templating();

		// Limité à 50 (DiscussionController l.227)
		const DISCUSSIONS = await Discussion.getAll();

		let content = await TEMPLATING.render(`timeline.html`, {discussions: DISCUSSIONS});

        response.setHeader("Content-Type", "text/html");
        response.setHeader("Content-Encoding", "gzip");
        const encoder = createGzip();
        encoder.pipe(response);
        encoder.write(content);
        encoder.end();
    }
}

export { TimelineController };
<file_sep>Une fois téléchargée, l’application se lance avec npm start.

port utilisé : 127.0.0.1.

https://github.com/Shigwen/OCR-Project-7


Vous trouverez la base de donnée utilisé comme exemple pour ce projet dans groupomania_social_network.sql

Notez que je n'ai compris que trop tard que les fichiers .gitignore doivent être présents avant le premier upload sur github.
Du coup, vous vous retrouvez sur github avec les node modules (bon, ça vous évite un npm install).

N'hésitez pas à me contacter si vous avez des soucis avec l'application. 
Notez que le serveur ferme tout seul s'il n'est pas utilisé pendant une certaines durée, 
et que vous serez automatiquement déconnecté de votre session utilisateur au bout de 5 minutes quoi qu'il arrive - 
ce qui vous ramènera à l'index à la moindre action.
<file_sep>import { createGzip } from "zlib";

import { Controller } from "../Model/Controller.js";
import { Templating } from "../Model/Templating.js";
import { Session as SessionService } from "../Service/Session.js";
import { Authentication as AuthenticationService } from "../Service/Authentication.js";

import { Discussion } from "../DataModel/Discussion.js";
import { Message } from "../DataModel/Message.js";

class DiscussionController extends Controller
{
	constructor()
	{
		super();
	}

	async newAction(request, response)
	{
		try
		{
			const SESSION = SessionService.getSession(request, response);
			const IS_LOGGED = AuthenticationService.checkUser(SESSION, response);

			if (!IS_LOGGED)
			{
				return;
			}

			const TEMPLATING = new Templating();
			let content = await TEMPLATING.render(`new_discussion.html`);

			response.setHeader("Content-Type", "text/html");
			response.setHeader("Content-Encoding", "gzip");
			const encoder = createGzip();
			encoder.pipe(response);
			encoder.write(content);
			encoder.end();
		}
		catch (error)
		{
			console.log(error);
			response.statusCode = 303;
			response.setHeader("Location", "/");
			response.setHeader("Content-Encoding", "gzip");
			const encoder = createGzip();
			encoder.pipe(response);
			encoder.end();
		}
	}

	async showAction(request, response)
	{
		try
		{
			const SESSION = SessionService.getSession(request, response);
			const IS_LOGGED = AuthenticationService.checkUser(SESSION, response);

			if (!IS_LOGGED)
			{
				return;
			}

			// Contiendra les variables de la route dans routing.json, ici id
			const GET = request.getQuery();
			// const DISCUSSION = await Discussion.getById(GET.id);

			const DISCUSSION = await Discussion.getById(GET.id);

			if (!DISCUSSION)
			{
				return;
			}

			const TITLE = await DISCUSSION.getTitle();
			const MESSAGES = await DISCUSSION.getMessages();

			// On peut déterminer ici l'ordre dans lequel les messages seront affichés, le premier message étant toujours en haut
			/*
			const MESSAGES = await DISCUSSION.getMessages(
				(a, b) =>
				{
					return b.getCreationDate() - a.getCreationDate();
				}
			);
			*/
			/*
			const MESSAGES = await DISCUSSION.getMessages(
				(a, b) =>
				{
					return b.getUpvote() - a.getUpvote();
				}
			);
			*/

			const TEMPLATING = new Templating();
			let content = await TEMPLATING.render(`discussion.html`, {messages: MESSAGES, title: TITLE, id: GET.id});

			response.setHeader("Content-Type", "text/html");
			response.setHeader("Content-Encoding", "gzip");
			const encoder = createGzip();
			encoder.pipe(response);
			encoder.write(content);
			encoder.end();
		}
		catch (error)
		{
			console.log(error);
			response.statusCode = 303;
			response.setHeader("Location", "/");
			response.setHeader("Content-Encoding", "gzip");
			const encoder = createGzip();
			encoder.pipe(response);
			encoder.end();
		}
	}

	async postAction(request, response)
	{
		try
		{
			const SESSION = SessionService.getSession(request, response);
			const IS_LOGGED = AuthenticationService.checkUser(SESSION, response);
			if (!IS_LOGGED)
			{
				return;
			}
			const POST = request.getRequest();
			const USER_ID = SESSION.get("userId");

			const NEW_DISCUSSION = new Discussion();

			NEW_DISCUSSION.setUserId(USER_ID);
			NEW_DISCUSSION.setFirstMessageId(null);
			NEW_DISCUSSION.setTitle(POST.title);
			await NEW_DISCUSSION.save();
			const NEW_DISCUSSION_ID = NEW_DISCUSSION.getId();

			const NEW_MESSAGE = new Message();

			NEW_MESSAGE.setUserId(USER_ID);
			NEW_MESSAGE.setDiscussionId(NEW_DISCUSSION_ID);
			NEW_MESSAGE.setContent(POST.content);
			await NEW_MESSAGE.save();

			NEW_DISCUSSION.setFirstMessageId(NEW_MESSAGE.getId());
			await NEW_DISCUSSION.save();

			response.statusCode = 303;
			response.setHeader("Location", "/timeline");
			response.setHeader("Content-Encoding", "gzip");
			const encoder = createGzip();
			encoder.pipe(response);
			encoder.end();
		}
		catch (error)
		{
			console.log(error);
			response.statusCode = 303;
			response.setHeader("Location", "/");
			response.setHeader("Content-Encoding", "gzip");
			const encoder = createGzip();
			encoder.pipe(response);
			encoder.end();
		}
	}
}
export { DiscussionController };
<file_sep>import { Server as HTTPSServer } from "https";
import { Request } from "./Request.js";
import { Routing } from "./Routing.js";
import { promises as FileSystem } from "fs";
class Server extends HTTPSServer {
    constructor(options, listener) {
        super(options, listener);
        this.port = 443;
    }
    handleRequest(message, response) {
        let request = new Request(message);
        let body = "";
        message.addListener("data", (chunk) => {
            body += chunk.toString("binary");
        });
        message.addListener("end", async () => {
            await request.setRawBody(body);
            try {
                await this.dispatchRequest(request, response);
            }
            catch (e) {
                console.log("Custom error:", e);
                response.statusCode = 404;
                response.end("404 - Not found.");
            }
        });
    }
    start() {
        this.listen(this.port);
    }
    async dispatchRequest(request, response) {
        const __DIRNAME__ = import.meta.url.replace(/^file:\/\/\/[A-Z]\:(.*)\/[^\/]+$/, "$1");
        const ROUTER = new Routing();
        await ROUTER.loadRoutingFile();
        const ROUTES = ROUTER.getRoutes();
        let controller_name;
		let action_name;
        await Promise.all(ROUTES.map(async (route) => {
            const MATCHES = request.getRequestedPath().match(route.regexp);
            if (MATCHES !== null) {
                const QUERY = {};
                const KEYS = Object.keys(route.variables);
                await Promise.all(KEYS.map((name) => {
                    const VARIABLE_REFERENCE = route.variables[name];
                    if (VARIABLE_REFERENCE.match(/^\$[0-9]+$/) === null) {
                    }
                    const INDEX = parseInt(VARIABLE_REFERENCE.substring(1));
                    if (MATCHES[INDEX] === undefined || MATCHES[INDEX] === null) {
                    }
                    QUERY[name] = MATCHES[INDEX];
                }));
                request.setQuery(QUERY);
                controller_name = `${route.controller}Controller`;
                action_name = `${route.action}Action`;
            }
		}));
        if (controller_name === undefined || action_name === undefined) {
            const PUBLIC_PATH = `${__DIRNAME__}/../../../www`;
            try {
                const POSSIBLE_FILE = await FileSystem.stat(`${PUBLIC_PATH}/${request.getRequestedPath()}`);
                if (!POSSIBLE_FILE.isFile()) {
                    throw new Error("Requested file does not exist");
                }
                const FILE = await FileSystem.readFile(`${PUBLIC_PATH}/${request.getRequestedPath()}`);
                response.write(FILE);
                response.end();
            }
            catch (e) {
                throw new Error("Requested file does not exist");
            }
        }
        else {
            try {
				const CLASS_PATH = `${__DIRNAME__}/../Controller/${controller_name}.js`;
				const FILE_STATS = await FileSystem.stat(CLASS_PATH);
                if (!FILE_STATS.isFile()) {
					throw new Error("The requested controller is not a file.");
				}
				const REQUESTED_CONTROLLER_CLASS = await import(CLASS_PATH);
				const REQUESTED_CONTROLLER = new REQUESTED_CONTROLLER_CLASS[controller_name];
                if (REQUESTED_CONTROLLER[action_name] !== undefined && REQUESTED_CONTROLLER[action_name] instanceof Function) {
					await REQUESTED_CONTROLLER[action_name](request, response);
                }
                else {
                }
            }
            catch (e) {
                console.log(e);
            }
        }
    }
}
export { Server };
<file_sep>
class Authentication
{
	static checkUser(session, response)
	{
		if (session.has("userId"))
		{
			return true;
		}
		else
		{
			response.statusCode = 307;
			response.setHeader("Location", "/");
			response.end();
			return false;
		}
	}
}

export { Authentication };
