Compare commits

..

4 commits

Author SHA1 Message Date
Nabil Ould Hamou
54aa98c088
fixed redisClient
<
2024-12-18 11:58:53 +01:00
Nabil Ould Hamou
9e064c220c
zz 2024-12-18 11:39:41 +01:00
e3181eff1f gitignore 2024-12-18 11:27:45 +01:00
2b894e62bb atelier prêt 2024-12-18 11:23:52 +01:00
17 changed files with 257 additions and 224 deletions

2
.env Normal file
View file

@ -0,0 +1,2 @@
DATABASE_URL="mongodb://temp-root-username:temp-password@localhost/chat_projetweb?authSource=admin"
JWT_SECRET="ba63466f102443f4bb6f3670891358bc4488d0c717f6ebcd3ee3c5144e55fe2d"

3
.gitignore vendored
View file

@ -8,14 +8,13 @@ node_modules
/.svelte-kit /.svelte-kit
/build /build
.idea .idea/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Env # Env
.env
.env.* .env.*
!.env.example !.env.example
!.env.test !.env.test

View file

@ -1,13 +0,0 @@
# Build stage
FROM node:18.18-alpine
WORKDIR /app
COPY . .
RUN npm i -g pnpm
EXPOSE 3000
RUN chmod +x /app/docker_entrypoint.sh
ENTRYPOINT ["/app/docker_entrypoint.sh"]

View file

@ -24,13 +24,29 @@ Commencez par cloner le dépôt Git sur votre machine
git clone https://github.com/NabilOuldHamou/M1_ProjetWeb git clone https://github.com/NabilOuldHamou/M1_ProjetWeb
``` ```
<h3>Production</h3> <h3>Pour travailler</h3>
Installez <bold>Docker</bold> sur votre machine puis utilisez la commande suivante pour lancer le projet Installez **Docker** sur votre machine puis utilisez la commande suivante pour lancer le projet
```bash ```bash
docker compose up docker compose up
``` ```
```bash
npm i -g pnpm
```
Installez les packets
```bash
pnpm i
```
Génération des fonctions prisma et envoie du schéma sur la base
```bash
pnpx prisma generate && pnpx prisma db push
```
<h2 id="auth">🤝 Auteurs</h2> <h2 id="auth">🤝 Auteurs</h2>
<table> <table>

View file

@ -1,23 +1,4 @@
services: services:
app:
build: .
hostname: app
depends_on:
- mongodb
- redis
environment:
- DATABASE_URL=mongodb://temp-root-username:temp-password@mongodb/chat_projetweb?authSource=admin
- JWT_SECRET=ba63466f102443f4bb6f3670891358bc4488d0c717f6ebcd3ee3c5144e55fe2d
- BODY_SIZE_LIMIT=Infinity
ports:
- "3000:3000"
networks:
- app_network
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules
mongodb: mongodb:
build: ./mongodb_rs build: ./mongodb_rs
hostname: mongodb hostname: mongodb
@ -26,7 +7,7 @@ services:
- MONGO_INITDB_ROOT_USERNAME=temp-root-username - MONGO_INITDB_ROOT_USERNAME=temp-root-username
- MONGO_INITDB_ROOT_PASSWORD=temp-password - MONGO_INITDB_ROOT_PASSWORD=temp-password
- MONGO_INITDB_DATABASE=chat_projetweb - MONGO_INITDB_DATABASE=chat_projetweb
- MONGO_REPLICA_HOST=mongodb - MONGO_REPLICA_HOST=localhost
- MONGO_REPLICA_PORT=27017 - MONGO_REPLICA_PORT=27017
ports: ports:
- "27017:27017" - "27017:27017"

View file

@ -1,13 +0,0 @@
#!/bin/sh
set -xe
pnpm install
pnpm prisma generate
pnpm run build
pnpx prisma db push
ls
node build

View file

@ -76,7 +76,7 @@
> >
<!-- Image de profil --> <!-- Image de profil -->
<img <img
src={`https://arbres.oxyjen.io/${user.profilePicture}`} src={`http://localhost:5173/${user.profilePicture}`}
alt="Profile Picture" alt="Profile Picture"
class="h-10 w-10 rounded-full border border-gray-300 cursor-pointer" class="h-10 w-10 rounded-full border border-gray-300 cursor-pointer"
/> />

View file

@ -26,7 +26,7 @@
<div class="overlay" role="dialog" aria-labelledby="profile-card-title" on:click={onClose}> <div class="overlay" role="dialog" aria-labelledby="profile-card-title" on:click={onClose}>
<div class="profile-card flex flex-col gap-5" on:click|stopPropagation> <div class="profile-card flex flex-col gap-5" on:click|stopPropagation>
<div class="profile-header"> <div class="profile-header">
<img src="https://arbres.oxyjen.io/{user.profilePicture}" alt="Profile" class="profile-image" /> <img src="http://localhost:5173/{user.profilePicture}" alt="Profile" class="profile-image" />
<h2 id="profile-card-title" class="profile-name">{user.username}</h2> <h2 id="profile-card-title" class="profile-name">{user.username}</h2>
</div> </div>
<div class="profile-info"> <div class="profile-info">

View file

@ -10,7 +10,7 @@
> >
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<img <img
src={`https://arbres.oxyjen.io/${user.profilePicture}`} src={`http://localhost:5173/${user.profilePicture}`}
alt="Profile" alt="Profile"
class="h-12 w-12 rounded-full border border-gray-300" class="h-12 w-12 rounded-full border border-gray-300"
/> />

View file

@ -1,7 +1,7 @@
import { createClient } from 'redis'; import { createClient } from 'redis';
const client = await createClient({ const client = await createClient({
url: process.env.REDIS_URL || 'redis://redis-server:6379' url: process.env.REDIS_URL || 'redis://localhost:6379'
}); });

View file

@ -2,7 +2,7 @@ import { io } from "socket.io-client";
// Initialisation de la socket // Initialisation de la socket
export const initSocket = () => { export const initSocket = () => {
const socketInstance = io("http://localhost:3000"); const socketInstance = io("http://localhost:5173");
let socketId = null; let socketId = null;
return socketInstance return socketInstance

View file

@ -4,7 +4,8 @@ import * as argon2 from 'argon2';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import logger from '$lib/logger'; import logger from '$lib/logger';
export async function POST({request}) { export async function POST({ request }) {
// Étape 1 : Récupérer les données du formulaire de la requête
const formData = await request.formData(); const formData = await request.formData();
// @ts-ignore // @ts-ignore
@ -12,35 +13,40 @@ export async function POST({request}) {
// @ts-ignore // @ts-ignore
const password: string = formData.get('password').toString(); const password: string = formData.get('password').toString();
// Étape 2 : Vérifier si l'utilisateur existe dans la base de données
const user = await prismaClient.user.findFirst({ const user = await prismaClient.user.findFirst({
where: { where: {
email: email, email: email,
} }
}); });
// Si l'utilisateur n'est pas trouvé, retourner une erreur
if (user == null) { if (user == null) {
logger.debug(`Could not find user with email (${email}) in database`); logger.debug(`Could not find user with email (${email}) in database`);
return error(400, {message: "Email ou mot de passe invalide."}); return error(400, { message: "Email ou mot de passe invalide." });
} }
// Étape 3 : Vérifier si le mot de passe est correct
logger.debug(`Found user with email (${email}) in database`); logger.debug(`Found user with email (${email}) in database`);
try { try {
if (await argon2.verify(user.password, password)) { if (await argon2.verify(user.password, password)) {
logger.debug(`Password for user ${user.email} is correct.`); logger.debug(`Password for user ${user.email} is correct.`);
// Étape 4 : Générer un token JWT pour l'utilisateur
// @ts-ignore // @ts-ignore
const token = jwt.sign(user, process.env.JWT_SECRET, { expiresIn: "1h" }); const token = jwt.sign(user, process.env.JWT_SECRET, { expiresIn: "1h" });
logger.debug(`Generated a JWT token for user ${user.email}.`) logger.debug(`Generated a JWT token for user ${user.email}.`);
return json({token: token, userId: user.id});
// Étape 5 : Retourner le token et l'ID de l'utilisateur
return json({ token: token, userId: user.id });
} else { } else {
return error(400, {message: "Email ou mot de passe invalide."}); return error(400, { message: "Email ou mot de passe invalide." });
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) { } catch (e) {
// Étape 6 : Gestion des erreurs
logger.error(e); logger.error(e);
return error(500, {message: e.body.message}); return error(500, { message: e.body.message });
} }
} }

View file

@ -4,52 +4,47 @@ import * as argon2 from 'argon2';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import logger from '$lib/logger'; import logger from '$lib/logger';
export async function POST({request}) { // POST: Créer un utilisateur et générer un token JWT
export async function POST({ request }) {
// Étape 1 : Récupérer les données du formulaire de la requête
const formData = await request.formData(); const formData = await request.formData();
// @ts-ignore const username: string = formData.get('username').toString().toLowerCase(); // Nom d'utilisateur en minuscules
const username: string = formData.get('username').toString().toLowerCase(); const email: string = formData.get('email').toString().toLowerCase(); // Email en minuscules
// @ts-ignore const password: string = formData.get('password').toString(); // Mot de passe brut
const email: string = formData.get('email').toString().toLowerCase();
// @ts-ignore
const password: string = formData.get('password').toString();
const user = await prismaClient.user.findFirst({ // Étape 2 : Vérifier si l'utilisateur existe déjà dans la base de données
where: { // Question 12 - A implémenter : Recuperer l'utilisateur avec le nom d'utilisateur ou l'email fourni.
OR: [ // - Utilisez la méthode await prismaClient.user.findFirst pour récupérer l'utilisateur par son nom d'utilisateur ou son email.
{ username: username }, // - Utilisez une clause OR pour rechercher l'utilisateur par nom d'utilisateur ou email.
{ email: email }, // - Stockez l'utilisateur dans une variable 'user' constante (const).
]
}
});
// Si l'utilisateur existe déjà, retourner une erreur
if (user != null) { if (user != null) {
logger.debug(`A user with email (${email}) already exists in database`); logger.debug(`A user with email (${email}) already exists in database`);
return error(400, {message: "Un compte avec cette adresse email ou nom d'utilisateur existe déjà."}); return error(400, { message: "Un compte avec cette adresse email ou nom d'utilisateur existe déjà." });
} }
try { try {
// Étape 3 : Hash du mot de passe
const hash = await argon2.hash(password); const hash = await argon2.hash(password);
const newUser = await prismaClient.user.create({ // Étape 4 : Créer un nouvel utilisateur dans la base de données
data: { // Question 13 - A implémenter : Utilisez `prismaClient.user.create` pour créer un utilisateur avec les données récupérées et le mot de passe hashé.
username: username, // - Utilisez await prismaClient.user.create pour créer un utilisateur avec les données fournies.
email: email, // - Incluez le nom d'utilisateur, l'email, le mot de passe hashé, le nom (par défaut vide) et le nom de famille (par défaut vide).
password: hash, // - Stockez le nouvel utilisateur dans une variable `newUser` (const).
surname: "",
name: ""
}
});
// @ts-ignore // Étape 5 : Générer un token JWT pour l'utilisateur
const token = jwt.sign(newUser, process.env.JWT_SECRET, { expiresIn: "1h" }); const token = jwt.sign(newUser, process.env.JWT_SECRET, { expiresIn: "1h" });
logger.debug(`Generated a JWT token for user ${newUser.email}.`) logger.debug(`Generated a JWT token for user ${newUser.email}.`);
return json({token: token, userId: newUser.id});
// Étape 6 : Retourner le token et l'ID de l'utilisateur créé
return json({ token: token, userId: newUser.id });
} catch (e) { } catch (e) {
// Étape 7 : Gestion des erreurs
logger.error(e); logger.error(e);
return error(500, {message: "Erreur interne."}); return error(500, { message: "Erreur interne." });
} }
} }

View file

@ -6,9 +6,11 @@ import { sortChannels } from '$lib/utils/sort.ts';
// GET: Liste tous les canaux avec leur premier message // GET: Liste tous les canaux avec leur premier message
export async function GET({ url }) { export async function GET({ url }) {
if(url.searchParams.get("name") != null && url.searchParams.get("name") != ""){ // Vérifier si un paramètre "name" est passé dans l'URL (pour filtrer par nom)
if (url.searchParams.get("name") != null && url.searchParams.get("name") != "") {
const name = url.searchParams.get("name"); const name = url.searchParams.get("name");
try { try {
// Étape 1 : Récupérer les canaux depuis la base de données (Prisma)
let canaux = await prisma.channel.findMany({ let canaux = await prisma.channel.findMany({
where: { where: {
name: { name: {
@ -18,118 +20,139 @@ export async function GET({ url }) {
}, },
include: { include: {
messages: { messages: {
take: 1, // Récupère le dernier message take: 1,
orderBy: { createdAt: 'desc' },// Trie par date décroissante orderBy: { createdAt: 'desc' },
// as lastMessage not list last message
}, },
}, },
}); });
// Étape 2 : Transformation des résultats
canaux = canaux.map((canaux) => { canaux = canaux.map((canaux) => {
return { return {
...canaux, ...canaux,
lastMessage: canaux.messages.length > 0 ? canaux.messages[0] : null, lastMessage: canaux.messages.length > 0 ? canaux.messages[0] : null,
messages: undefined messages: undefined,
}; };
}); });
canaux = sortChannels(canaux); // Étape 3 : Trier les canaux par date du dernier message
canaux = sortChannels(canaux); // Fonction pour trier les canaux
return json(canaux); return json(canaux);
} catch (err) { } catch (err) {
// Gérer les erreurs et renvoyer une réponse d'erreur en cas de problème
logger.error(err); logger.error(err);
return json({ error: 'Erreur serveur' }, { status: 500 }); return json({ error: 'Erreur serveur' }, { status: 500 });
} }
}else{ } else {
try { try {
// Étape 4 : Vérifier si les canaux sont dans le cache Redis
let channels = []; let channels = [];
// Question 8 - A implémenter : Récupérer les canaux dans le cache Redis.
const cachedChannels = await redisClient.get('channels'); // - A l'aide de la clé 'channels', récupérez les canaux depuis le cache Redis.
// - Utilisez await redisClient.get pour récupérer les canaux depuis le cache Redis.
// - Stockez les canaux dans une variable 'cachedChannels' constante (const).
if (cachedChannels != null) { if (cachedChannels != null) {
logger.debug('Cache entry found, fetching channels from cache'); logger.debug('Cache entry found, fetching channels from cache');
channels = JSON.parse(cachedChannels); channels = JSON.parse(cachedChannels); // Charger les canaux depuis le cache
}else{ } else {
logger.debug('No cache entry was found, fetching channels from database'); logger.debug('No cache entry was found, fetching channels from database');
} }
if(channels.length < 10){
logger.debug('Fetching channels from database to fill cache');
let canaux = await prisma.channel.findMany({
include: {
messages: {
take: 1, // Récupère le dernier message
orderBy: { createdAt: 'desc' }, // Trie par date décroissante
},
},
});
// Étape 5 : Si le cache est insuffisant, récupérer les canaux depuis la base de données
if (channels.length < 10) {
logger.debug('Fetching channels from database to fill cache');
// Question 9 - A implémenter : Récupérer les canaux dans la base de donnée mongoDB.
// - Utilisez Prisma pour récupérer les canaux depuis MongoDB.
// - Utilisez la méthode await prisma.channel.findMany pour récupérer les canaux.
// - Incluez le dernier message des canaux.
// - Stockez les canaux dans une variable 'canaux' (let).
// Transformation des canaux pour ne garder que le dernier message
canaux = canaux.map((canaux) => { canaux = canaux.map((canaux) => {
return { return {
...canaux, ...canaux,
lastMessage: canaux.messages.length > 0 ? canaux.messages[0] : null, lastMessage: canaux.messages.length > 0 ? canaux.messages[0] : null,
messages: undefined messages: undefined, // Ne pas retourner la liste complète des messages
}; };
}); });
// Fusionner les nouveaux canaux récupérés avec les canaux existants dans le cache
channels = channels.concat(canaux); channels = channels.concat(canaux);
// Supprimer les doublons en vérifiant par l'ID du canal
channels = channels.filter((channel, index, self) => channels = channels.filter((channel, index, self) =>
index === self.findIndex((t) => ( index === self.findIndex((t) => (
t.id === channel.id t.id === channel.id
)) ))
); );
// Trier les canaux par la date du dernier message
channels = sortChannels(channels); channels = sortChannels(channels);
// Limiter à 10 canaux maximum
channels = channels.slice(0, 10); channels = channels.slice(0, 10);
await redisClient.set('channels', JSON.stringify(channels), { EX: 3600 }); // Mettre à jour le cache Redis avec les canaux
await redisClient.set('channels', JSON.stringify(channels), { EX: 3600 }); // Cache pendant 1 heure
} }
return json(channels); return json(channels); // Retourner les canaux sous forme JSON
} catch (err) { } catch (err) {
logger.error(err) // Gérer les erreurs et renvoyer une réponse d'erreur en cas de problème
logger.error(err);
return json({ error: 'Erreur serveur' }, { status: 500 }); return json({ error: 'Erreur serveur' }, { status: 500 });
} }
} }
} }
// POST: Créer un nouveau canal et mettre à jour le cache Redis
export async function POST({ request }) { export async function POST({ request }) {
// Étape 1 : Récupérer les données de la requête (nom du canal)
const { name } = await request.json(); const { name } = await request.json();
try { try {
let canal = await prisma.channel.create({ // Étape 2 : Créer un nouveau canal dans la base de données
data: { // Question 10 - A implémenter : Utilisez `prisma.channel.create` pour créer un canal avec le nom fourni.
name // - Dans une variable `canal` (let) , stockez le résultat de la création du canal.
},
});
logger.debug('Creating a new channel in database with id ' + canal.id); logger.debug('Creating a new channel in database with id ' + canal.id);
// Étape 3 : Mettre à jour le cache Redis des canaux
const cachedChanels = await redisClient.get('channels'); const cachedChanels = await redisClient.get('channels');
let channels = cachedChanels != null ? JSON.parse(cachedChanels) : []; let channels = cachedChanels != null ? JSON.parse(cachedChanels) : [];
// Étape 4 : Structurer les données du canal à ajouter au cache
canal = { canal = {
...canal, ...canal,
lastMessage: null, lastMessage: null,
lastUpdate: canal.createdAt, lastUpdate: canal.createdAt,
messages: undefined messages: undefined,
} }
// Étape 5 : Ajouter le canal au tableau des canaux et trier
channels.push(canal); channels.push(canal);
// Trier les canaux par la dernière mise à jour
channels = sortChannels(channels); channels = sortChannels(channels);
// Étape 6 : Mettre à jour le cache Redis avec la nouvelle liste de canaux
logger.debug(`Added channel (${canal.id}) to channels cache.`); logger.debug(`Added channel (${canal.id}) to channels cache.`);
await redisClient.set('channels', JSON.stringify(channels), { EX: 600 }); // Question 11 - A implémenter : Mettez à jour le cache Redis avec la liste des canaux, avec une expiration de 10 minutes.
// - Utilisez await redisClient.set pour mettre à jour le cache des canaux.
// - Utilisez une expiration de 10 minutes (600 secondes).
// - Stockez les canaux dans le cache Redis en utilisant la clé 'channels'.
// - Utilisez JSON.stringify pour convertir les canaux en chaîne JSON.
// Étape 7 : Retourner la réponse avec le canal créé
return json(canal, { status: 201 }); return json(canal, { status: 201 });
} catch (err) { } catch (err) {
// Étape 8 : Gérer les erreurs en cas de problème
console.log(err); console.log(err);
logger.error(err); logger.error(err);
return json({ error: 'Erreur lors de la création du canal' }, { status: 500 }); return json({ error: 'Erreur lors de la création du canal' }, { status: 500 });
@ -137,3 +160,4 @@ export async function POST({ request }) {
} }

View file

@ -3,46 +3,56 @@ import prisma from '$lib/prismaClient';
import redisClient from '$lib/redisClient'; import redisClient from '$lib/redisClient';
import logger from '$lib/logger'; import logger from '$lib/logger';
// Récupérer les informations du canal et le dernier message (avec cache Redis) // GET: Récupérer les informations d'un canal
export async function GET({ params }) { export async function GET({ params }) {
const channelId = params.id; const channelId = params.id;
// Définir la clé de cache Redis pour le canal
const channelCacheKey = `channel:${channelId}:info`; const channelCacheKey = `channel:${channelId}:info`;
try { try {
// Étape 1 : Vérifier si les informations du canal sont en cache Redis
const cachedChannel = await redisClient.get(channelCacheKey); const cachedChannel = await redisClient.get(channelCacheKey);
if (cachedChannel) { if (cachedChannel) {
// Si le canal est dans le cache, renvoyez-le
logger.debug(`Cache entry found, fetching channel (${channelId}) from cache`); logger.debug(`Cache entry found, fetching channel (${channelId}) from cache`);
return json(JSON.parse(cachedChannel)); return json(JSON.parse(cachedChannel));
} }
// Étape 2 : Si le canal n'est pas dans le cache, le récupérer depuis la base de données MongoDB
logger.debug(`No cache entry was found, fetching channel (${channelId}) from database`); logger.debug(`No cache entry was found, fetching channel (${channelId}) from database`);
const canal = await prisma.channel.findUnique({
where: { id: channelId },
});
// Question 6 - A implémenter : Récupérer les informations du canal depuis la base de données.
// Récupérer les informations du canal depuis la base de données
// Utilisez la méthode await prisma.channel.findUnique pour récupérer le canal par son ID.
// Stockez les informations du canal dans une variable 'canal' constante (const).
// Vérifier si le canal existe dans la base de données
if (!canal) { if (!canal) {
logger.debug(`No channel for id ${channelId} was found in database`) logger.debug(`No channel for id ${channelId} was found in database`);
return json({ error: 'Canal non trouvé' }, { status: 404 }); return json({ error: 'Canal non trouvé' }, { status: 404 });
} }
const lastMessage = await prisma.message.findFirst({ // Étape 3 : Récupérer le dernier message du canal
where: { id: channelId }, // Question 7 - A implémenter : Récupérer le dernier message du canal depuis la base de données.
orderBy: { createdAt: 'desc' }, // Utilisez la méthode await prisma.message.findFirst pour récupérer le dernier message du canal.
}); // Filtrez les messages par `channelId` et triez-les par date de création décroissante.
// Stockez le dernier message dans une variable 'lastMessage' constante (const).
// Créer un objet combiné pour le canal et le dernier message // Créer un objet combiné pour le canal et son dernier message
const canalData = { const canalData = {
canal, canal,
lastMessage, // Inclure uniquement le dernier message lastMessage,
}; };
// Étape 4 : Mettre à jour le cache global des canaux dans Redis
const cachedChannels = await redisClient.get('channels');
let channels = cachedChannels != null ? JSON.parse(cachedChannels) : [];
const cachedChanels = await redisClient.get('channels'); // Ajouter le canal actuel dans la liste des canaux
let channels = cachedChanels != null ? JSON.parse(cachedChanels) : [];
channels.push(canal); channels.push(canal);
// Trier les canaux par la date du dernier message
channels = channels.sort( channels = channels.sort(
( (
a: { messages: { createdAt: Date }[]; createdAt: Date }, a: { messages: { createdAt: Date }[]; createdAt: Date },
@ -54,16 +64,19 @@ export async function GET({ params }) {
} }
); );
// Enregistrer la liste mise à jour des canaux dans Redis
logger.debug(`Added channel (${canal.id}) to channels cache.`); logger.debug(`Added channel (${canal.id}) to channels cache.`);
await redisClient.set('channels', JSON.stringify(channels), { EX: 600 }); await redisClient.set('channels', JSON.stringify(channels), { EX: 600 });
// Étape 5 : Ajouter les informations du canal et du dernier message dans le cache Redis
logger.debug(`Creating a new cache entry with key channel:${channelId}:info`); logger.debug(`Creating a new cache entry with key channel:${channelId}:info`);
await redisClient.set(channelCacheKey, JSON.stringify(canalData), {EX: 600, NX: true}); // Cache pendant 5 minutes await redisClient.set(channelCacheKey, JSON.stringify(canalData), { EX: 600, NX: true });
// Étape 6 : Retourner les données du canal et du dernier message
return json(canalData); return json(canalData);
} catch (err) {
} catch (err) {
// Si une erreur survient lors de la récupération des informations du canal ou du dernier message, retournez une erreur
logger.error(err); logger.error(err);
return json({ error: 'Erreur lors de la récupération du canal ou du dernier message' }, { status: 500 }); return json({ error: 'Erreur lors de la récupération du canal ou du dernier message' }, { status: 500 });
} }

View file

@ -4,6 +4,7 @@ import redisClient from '$lib/redisClient';
import logger from '$lib/logger'; import logger from '$lib/logger';
import { sortChannels } from '$lib/utils/sort.ts'; import { sortChannels } from '$lib/utils/sort.ts';
// GET: Liste tous les messages d'un canal avec pagination
export async function GET({ params, url }) { export async function GET({ params, url }) {
const channelId = params.id; const channelId = params.id;
logger.debug(`GET /api/channels/${channelId}/messages`); logger.debug(`GET /api/channels/${channelId}/messages`);
@ -14,16 +15,17 @@ export async function GET({ params, url }) {
try { try {
logger.debug(`Tentative de récupération des messages du cache pour le channel : ${channelId}`); logger.debug(`Tentative de récupération des messages du cache pour le channel : ${channelId}`);
let redisMessageKeys = await redisClient.zRangeWithScores(
`channel:${channelId}:messages`,
offset,
offset + limit - 1,
{ REV: true }
);
// Étape 1 : Récupérer les clés Redis des messages
// Question 1 - A implémenter : Récupérer les messages depuis Redis avec la pagination.
// - Utilisez await redisClient.zRangeWithScores pour récupérer les clés des messages dans une variable redisMessageKeys (let).
// - La clé Redis pour l'ensemble trié des messages est sous la forme : `channel:<channelId>:messages`.
// - Vous devez récupérer les messages selon l'offset et le limit.
//Suppression des messages dans le cache si message:<id> n'existe plus
const redisPipelineRemove = redisClient.multi(); const redisPipelineRemove = redisClient.multi();
for (const messageKey of redisMessageKeys) { for (const messageKey of redisMessageKeys) {
// Vérifie si la clé existe dans Redis // Vérifie si la clé existe dans Redis
const messageKeyValue = messageKey.value; const messageKeyValue = messageKey.value;
@ -36,6 +38,7 @@ export async function GET({ params, url }) {
} }
await redisPipelineRemove.exec(); await redisPipelineRemove.exec();
// Étape 2 : Si des messages sont trouvés dans Redis
if (redisMessageKeys.length > 0) { if (redisMessageKeys.length > 0) {
const messages = await Promise.all( const messages = await Promise.all(
redisMessageKeys.map(async (key) => { redisMessageKeys.map(async (key) => {
@ -44,11 +47,12 @@ export async function GET({ params, url }) {
}) })
); );
// Met à jour le TTL pour les messages et l'ensemble trié
const redisPipeline = redisClient.multi(); const redisPipeline = redisClient.multi();
for (const key of redisMessageKeys) { for (const key of redisMessageKeys) {
const message = await redisClient.get(key.value); const message = await redisClient.get(key.value);
const msg = JSON.parse(message) const msg = JSON.parse(message);
redisPipeline.set(key.value, JSON.stringify(msg), {EX: 1800}); redisPipeline.set(key.value, JSON.stringify(msg), { EX: 1800 }); // TTL 30 minutes
redisPipeline.zAdd(`channel:${channelId}:messages`, { redisPipeline.zAdd(`channel:${channelId}:messages`, {
score: key.score, score: key.score,
value: key.value, value: key.value,
@ -59,35 +63,28 @@ export async function GET({ params, url }) {
return json({ limit, page, messages: messages.reverse() }); return json({ limit, page, messages: messages.reverse() });
} }
// Étape 3 : Aucun message trouvé dans Redis, récupération depuis MongoDB
logger.debug(`Aucun message trouvé dans le cache, récupération depuis MongoDB pour le channel : ${channelId}`); logger.debug(`Aucun message trouvé dans le cache, récupération depuis MongoDB pour le channel : ${channelId}`);
const messagesFromDB = await prisma.message.findMany({ // Question 2 - A implémenter : Si aucun message n'est trouvé dans Redis, récupérez-les depuis MongoDB.
where: { channelId }, // - Utiliser Prisma pour récupérer les messages depuis MongoDB.
select: { // - Utilisez la méthode await prisma.message.findMany de Prisma pour récupérer les messages.
id: true, // - Filtrez les messages par `channelId` et recuperer l'id, le createdAt, le text, le user ( avec son id ).
createdAt: true, // - Appliquez la pagination avec `skip` et `take` pour gérer le `offset` et le `limit`.
text: true, // - Triez les messages par date de création décroissante.
user: { // - Stockez les messages dans une variable `messagesFromDB` constante (const).
select: {
id: true,
},
},
},
orderBy: { createdAt: 'desc' },
skip: offset,
take: limit,
});
// Étape 4 : Si des messages sont récupérés depuis MongoDB, les stocker dans Redis
if (messagesFromDB.length > 0) { if (messagesFromDB.length > 0) {
const redisPipeline = redisClient.multi(); const redisPipeline = redisClient.multi();
for (const message of messagesFromDB) { for (const message of messagesFromDB) {
const messageKey = `message:${message.id}`; const messageKey = `message:${message.id}`;
redisPipeline.set(messageKey, JSON.stringify(message), {EX: 1800}); redisPipeline.set(messageKey, JSON.stringify(message), { EX: 1800 }); // TTL 30 minutes
redisPipeline.zAdd(`channel:${channelId}:messages`, { redisPipeline.zAdd(`channel:${channelId}:messages`, {
score: new Date(message.createdAt).getTime(), score: new Date(message.createdAt).getTime(),
value: messageKey, value: messageKey,
}); });
} }
await redisPipeline.exec(); await redisPipeline.exec();
} }
@ -98,48 +95,36 @@ export async function GET({ params, url }) {
} }
} }
// Fonction POST permettant de créer un nouveau message
export async function POST({ params, request }) { export async function POST({ params, request }) {
const channelId = params.id; const channelId = params.id;
const { userId, text } = await request.json(); const { userId, text } = await request.json();
try { try {
// Créer un nouveau message dans MongoDB // Étape 1 : Créer un nouveau message dans MongoDB
let newMessage = await prisma.message.create({ // Question 3 - A implémenter : Utilisez Prisma pour créer un nouveau message dans MongoDB.
data: { // - Utilisez await prisma.message.create pour créer un message avec les données fournies.
userId, // - Incluez l'id, la date de création, le text, l'utilisateur (avec son id), et le canal (avec son id et son name).
channelId, // - Stockez le message dans une variable `newMessage` (let).
text, // - Utiliser userId, channelId et text pour créer le message.
},
select: {
id: true,
createdAt: true,
text: true,
user: {
select: {
id: true,
},
},
channel: {
select: {
id: true,
name: true,
},
}
},
});
// Ajouter le message dans Redis // Étape 2 : Ajouter le message dans Redis
await redisClient.set(`message:${newMessage.id}`, JSON.stringify(newMessage), {EX: 1800}); // Question 4 - A implémenter : Stocker le message dans Redis.
await redisClient.zAdd(`channel:${channelId}:messages`, { // - Utiliser await redisClient.set pour ajouter le message dans Redis.
score: new Date(newMessage.createdAt).getTime(), // - Avec un Time To Live de 30 Minutes (1800 secondes)
value: `message:${newMessage.id}`, // - Assurez-vous de structurer la clé comme suit : `message:<messageId>`.
});
//update the channels cache with the new message // Question 5 - A implémenter : Ajouter la clé du message dans la liste ordonnée des messages du canal.
// - Utiliser await redisClient.zAdd pour insérer le message dans un ensemble trié basé sur la date de création.
// Assurez-vous de structurer la clé comme suit : `channel:<channelId>:messages`
// Étape 3 : Mettre à jour le cache des channels avec le nouveau message
const cachedChannels = await redisClient.get('channels'); const cachedChannels = await redisClient.get('channels');
let channels = cachedChannels ? JSON.parse(cachedChannels) : []; let channels = cachedChannels ? JSON.parse(cachedChannels) : [];
let channel = channels.find((c) => c.id === channelId); let channel = channels.find((c) => c.id === channelId);
if(channel){
if (channel) {
channel.lastMessage = { channel.lastMessage = {
id: newMessage.id, id: newMessage.id,
text: newMessage.text, text: newMessage.text,
@ -149,13 +134,18 @@ export async function POST({ params, request }) {
channel.lastUpdate = newMessage.createdAt; channel.lastUpdate = newMessage.createdAt;
channel.messages = undefined; channel.messages = undefined;
}else{ } else {
channel = {...newMessage.channel, lastMessage: { channel = {
...newMessage.channel,
lastMessage: {
id: newMessage.id, id: newMessage.id,
text: newMessage.text, text: newMessage.text,
user: newMessage.user, user: newMessage.user,
createdAt: newMessage.createdAt, createdAt: newMessage.createdAt,
}, lastUpdate: newMessage.createdAt, messages: undefined}; },
lastUpdate: newMessage.createdAt,
messages: undefined
};
channels = [channel, ...channels]; channels = [channel, ...channels];
} }
await redisClient.set('channels', JSON.stringify(channels), { EX: 600 }); await redisClient.set('channels', JSON.stringify(channels), { EX: 600 });
@ -170,12 +160,14 @@ export async function POST({ params, request }) {
logger.debug(`Nouveau message ajouté pour le channel : ${channelId}`); logger.debug(`Nouveau message ajouté pour le channel : ${channelId}`);
return json(newMessage, { status: 201 }); return json(newMessage, { status: 201 });
} catch (err) { } catch (err) {
logger.error(`Erreur lors de la création du message : ${err.message}`); logger.error(`Erreur lors de la création du message : ${err.message}`);
return json({ error: 'Erreur lors de la création du message' }, { status: 500 }); return json({ error: 'Erreur lors de la création du message' }, { status: 500 });
} }
} }
export async function DELETE({ params, request }) { export async function DELETE({ params, request }) {
const channelId = params.id; const channelId = params.id;
const { messageId } = await request.json(); const { messageId } = await request.json();

View file

@ -4,34 +4,52 @@ import redisClient from '$lib/redisClient';
import prisma from '$lib/prismaClient'; import prisma from '$lib/prismaClient';
import logger from '$lib/logger'; import logger from '$lib/logger';
// GET: Récupérer tous les utilisateurs avec cache Redis
export async function GET() { export async function GET() {
try { try {
// Vérifier si les utilisateurs sont dans le cache Redis // Étape 1 : Vérifier si les utilisateurs sont déjà présents dans le cache Redis
const cachedUsers = await redisClient.get('users'); const cachedUsers = await redisClient.get('users');
// Si les utilisateurs sont trouvés dans le cache, renvoyez-les directement
if (cachedUsers) { if (cachedUsers) {
// Si le cache contient les utilisateurs, on les récupère et on les renvoie
logger.debug('Cache entry found, fetching users from cache'); logger.debug('Cache entry found, fetching users from cache');
return json(JSON.parse(cachedUsers)); return json(JSON.parse(cachedUsers));
} }
// Étape 2 : Si les utilisateurs ne sont pas dans le cache, les récupérer depuis la base de données
logger.debug('No cache entry was found, fetching users from database'); logger.debug('No cache entry was found, fetching users from database');
// Sinon, récupérer les utilisateurs depuis MongoDB
// Question 14 - A implémenter : Utilisez Prisma pour récupérer la liste des utilisateurs dans la base de données.
// - Utilisez la méthode await prisma.user.findMany pour récupérer la liste des utilisateurs.
// - Stockez les utilisateurs dans une variable 'users' constante (const).
const users = await prisma.user.findMany(); const users = await prisma.user.findMany();
// Mettre les utilisateurs en cache // Étape 3 : Mettre en cache les utilisateurs récupérés depuis la base de données
logger.debug('Caching users with EX of 600 secs'); logger.debug('Caching users with EX of 600 secs');
await redisClient.set('users', JSON.stringify(users), { EX: 600 });
// Question 15 - A implémenter : Mettre dans le cache Redis avec une expiration de 600 secondes.
// - Utilisez await redisClient.set pour mettre en cache les utilisateurs.
// - Utilisez la méthode JSON.stringify pour convertir les utilisateurs en format JSON.
// - Utilisez l'option { EX: 600 } pour définir l'expiration
// Étape 4 : Retourner la liste des utilisateurs récupérés
return json(users); return json(users);
} catch (err) { } catch (err) {
// Étape 5 : Gestion des erreurs
logger.error(err); logger.error(err);
return json({ error: 'Erreur serveur' }, { status: 500 }); return json({ error: 'Erreur serveur' }, { status: 500 });
} }
} }
// POST: Créer un utilisateur et mettre à jour le cache
export async function POST({ request }) { export async function POST({ request }) {
// Étape 1 : Récupérer les données envoyées dans la requête (username, surname, name, email, password)
const { username, surname, name, email, password } = await request.json(); const { username, surname, name, email, password } = await request.json();
try { try {
// Étape 2 : Créer un nouvel utilisateur dans la base de données
const user = await prisma.user.create({ const user = await prisma.user.create({
data: { data: {
username: username.toLowerCase(), username: username.toLowerCase(),
@ -41,22 +59,35 @@ export async function POST({ request }) {
password, password,
}, },
}); });
// Étape 3 : Loguer la création de l'utilisateur avec son ID
logger.debug('Creating a new user in database with id ' + user.id); logger.debug('Creating a new user in database with id ' + user.id);
// Mettre le nouvel utilisateur dans le cache // Étape 4 : Mettre à jour le cache global des utilisateurs
logger.debug(`Caching user (${user.id})`); logger.debug(`Caching user (${user.id})`);
const cachedUsers = await redisClient.get('users'); const cachedUsers = await redisClient.get('users');
const usersArray = cachedUsers != null ? JSON.parse(cachedUsers) : []; const usersArray = cachedUsers != null ? JSON.parse(cachedUsers) : [];
usersArray.push(user); usersArray.push(user);
// Étape 5 : Enregistrer la liste mise à jour des utilisateurs dans le cache Redis
logger.debug(`Added user (${user.id}) to users cache.`); logger.debug(`Added user (${user.id}) to users cache.`);
await redisClient.set('users', JSON.stringify(usersArray), { EX: 600 }) await redisClient.set('users', JSON.stringify(usersArray), { EX: 600 });
logger.debug(`Creating a new cache entry with key user:${user.id}, with EX of 3600 secs`);
await redisClient.set(`user:${user.id}`, JSON.stringify(user), { EX: 3600 });
// Étape 6 : Créer un cache individuel pour cet utilisateur spécifique
logger.debug(`Creating a new cache entry with key user:${user.id}, with EX of 3600 secs`);
// Question 16 - A implémenter : Mettre en cache l'utilisateur créé avec une expiration de 1 heure (3600 secondes).
// - Utilisez await redisClient.set pour mettre en cache l'utilisateur.
// - Utilisez JSON.stringify pour convertir l'utilisateur en format JSON.
// - Utilisez l'option { EX: 3600 } pour définir l'expiration.
// - Utilisez la clé 'user:${user.id}' pour stocker l'utilisateur dans le cache.
// Étape 7 : Retourner l'utilisateur créé avec un statut 201
return json(user, { status: 201 }); return json(user, { status: 201 });
} catch (err) { } catch (err) {
logger.error(err) // Étape 8 : Gestion des erreurs
logger.error(err);
return json({ error: 'Erreur lors de la création de lutilisateur' }, { status: 500 }); return json({ error: 'Erreur lors de la création de lutilisateur' }, { status: 500 });
} }
} }