Chargement...
;
}
- const handleSaveImage = async () => {
- try {
- const inputElement = document.getElementById('file-input') as HTMLInputElement;
-
- if (!inputElement?.files || inputElement.files.length === 0) {
- setDialogOpen(true);
- return;
- }
-
- if (!inputElement.files || inputElement.files.length === 0) {
- window.alert("Veuillez d'abord choisir une image à téléverser.")
- return;
- }
-
- const imageUrl = await ApiService.uploadImage(inputElement.files[0]);
-
- // Check for errors
- if(imageUrl.indexOf("ERROR") >= 0) {
- window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
- return;
- }
-
- setImageLinks(prevLinks => [...prevLinks, imageUrl]);
-
- // Reset the file input element
- if (fileInputRef.current) {
- fileInputRef.current.value = '';
- }
- } catch (error) {
- window.alert(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`)
-
- }
- };
-
const handleCopyToClipboard = async (link: string) => {
navigator.clipboard.writeText(link);
}
+ const handleCopyImage = (id: string) => {
+ const escLink = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`;
+ navigator.clipboard.writeText(id);
+ setImageLinks(prevLinks => [...prevLinks, escLink]);
+ }
+
return (
@@ -258,37 +229,11 @@ const QuizForm: React.FC = () => {
onEditorChange={handleUpdatePreview} />
-
-
-
+
+
Mes images :
+
-
-
Mes images :
+
(Voir section
@@ -302,7 +247,7 @@ const QuizForm: React.FC = () => {
{imageLinks.map((link, index) => {
- const imgTag = `} "texte de l'infobulle")`;
+ const imgTag = `[markdown]} "texte de l'infobulle") {T}`;
return (
-
{
const url: string = this.constructRequestUrl(`/auth/getRoomsRequireAuth`);
@@ -1167,7 +1184,99 @@ public async login(email: string, password: string): Promise {
return `ERROR : Une erreur inattendue s'est produite.`
}
}
- // NOTE : Get Image pas necessaire
+
+ public async getImages(page: number, limit: number): Promise {
+ try {
+ const url: string = this.constructRequestUrl(`/image/getImages`);
+ const headers = this.constructRequestHeaders();
+ let params : ImagesParams = { page: page, limit: limit };
+
+ const result: AxiosResponse = await axios.get(url, { params: params, headers: headers });
+
+ if (result.status !== 200) {
+ throw new Error(`L'affichage des images a échoué. Status: ${result.status}`);
+ }
+ const images = result.data;
+
+ return images;
+
+ } catch (error) {
+ console.log("Error details: ", error);
+
+ if (axios.isAxiosError(error)) {
+ const err = error as AxiosError;
+ const data = err.response?.data as { error: string } | undefined;
+ const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
+ throw new Error(`L'enregistrement a échoué. Status: ${msg}`);
+ }
+
+ throw new Error(`ERROR : Une erreur inattendue s'est produite.`);
+ }
+ }
+
+ public async getUserImages(page: number, limit: number): Promise {
+ try {
+ const url: string = this.constructRequestUrl(`/image/getUserImages`);
+ const headers = this.constructRequestHeaders();
+ let params : ImagesParams = { page: page, limit: limit };
+
+ const uid = this.getUserID();
+ if(uid !== ''){
+ params.uid = uid;
+ }
+
+ const result: AxiosResponse = await axios.get(url, { params: params, headers: headers });
+
+ if (result.status !== 200) {
+ throw new Error(`L'affichage des images de l'utilisateur a échoué. Status: ${result.status}`);
+ }
+ const images = result.data;
+
+ return images;
+
+ } catch (error) {
+ console.log("Error details: ", error);
+
+ if (axios.isAxiosError(error)) {
+ const err = error as AxiosError;
+ const data = err.response?.data as { error: string } | undefined;
+ const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
+ throw new Error(`L'enregistrement a échoué. Status: ${msg}`);
+ }
+
+ throw new Error(`ERROR : Une erreur inattendue s'est produite.`);
+ }
+ }
+
+ public async deleteImage(imgId: string): Promise {
+ try {
+ const url: string = this.constructRequestUrl(`/image/delete`);
+ const headers = this.constructRequestHeaders();
+ const uid = this.getUserID();
+ let params = { uid: uid, imgId: imgId };
+
+ const result: AxiosResponse = await axios.delete(url, { params: params, headers: headers });
+
+ if (result.status !== 200) {
+ throw new Error(`La suppression de l'image a échoué. Status: ${result.status}`);
+ }
+
+ const deleted = result.data.deleted;
+ return deleted;
+
+ } catch (error) {
+ console.log("Error details: ", error);
+
+ if (axios.isAxiosError(error)) {
+ const err = error as AxiosError;
+ const data = err.response?.data as { error: string } | undefined;
+ const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
+ throw new Error(`L'enregistrement a échoué. Status: ${msg}`);
+ }
+
+ throw new Error(`ERROR : Une erreur inattendue s'est produite.`);
+ }
+ }
}
diff --git a/docker-compose-local.yaml b/docker-compose-local.yaml
index 0d8d61a..b77c340 100644
--- a/docker-compose-local.yaml
+++ b/docker-compose-local.yaml
@@ -7,6 +7,8 @@ services:
context: ./client
dockerfile: Dockerfile
container_name: frontend
+ environment:
+ VITE_IMG_URL: http://localhost
ports:
- "5173:5173"
restart: always
@@ -54,7 +56,7 @@ services:
image: mongo
container_name: mongo
ports:
- - "27017:27017"
+ - "27019:27017"
tty: true
volumes:
- mongodb_data:/data/db
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 539c800..d133982 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -10,6 +10,7 @@ services:
- VITE_BACKEND_URL=
# Define empty VITE_BACKEND_SOCKET_URL so it will default to window.location.host
- VITE_BACKEND_SOCKET_URL=
+ - VITE_IMG_URL=https://evalsa.etsmtl.ca
ports:
- "5173:5173"
restart: always
diff --git a/server/__tests__/image.test.js b/server/__tests__/image.test.js
index 488d27b..7ce4ad1 100644
--- a/server/__tests__/image.test.js
+++ b/server/__tests__/image.test.js
@@ -7,6 +7,9 @@
// const BASE_URL = '/image'
+const Images = require('../models/images');
+const ObjectId = require('mongodb').ObjectId;
+
describe.skip("POST /upload", () => {
describe("when the jwt is not sent", () => {
@@ -64,3 +67,289 @@ describe.skip("GET /get", () => {
})
})
+
+jest.mock('mongodb', () => {
+ const originalModule = jest.requireActual('mongodb');
+ return {
+ ...originalModule,
+ ObjectId: {
+ ...originalModule.ObjectId,
+ createFromHexString: jest.fn().mockReturnValue('507f191e810c19729de860ea'), // Return a valid 24-character ObjectId string
+ },
+ };
+});
+
+describe('Images', () => {
+ let db;
+ let images;
+ let dbConn;
+ let mockImagesCollection;
+ let mockFindCursor;
+
+ beforeEach(() => {
+
+ const mockImagesCursor = {
+ sort: jest.fn().mockReturnThis(),
+ skip: jest.fn().mockReturnThis(),
+ limit: jest.fn().mockReturnThis(),
+ toArray: jest.fn()
+ };
+
+ const mockFilesCursor = {
+ sort: jest.fn().mockReturnThis(),
+ skip: jest.fn().mockReturnThis(),
+ limit: jest.fn().mockReturnThis(),
+ toArray: jest.fn()
+ };
+
+ mockImagesCollection = {
+ insertOne: jest.fn().mockResolvedValue({ insertedId: 'image123' }),
+ findOne: jest.fn(),
+ find: jest.fn().mockReturnValue(mockImagesCursor),
+ countDocuments: jest.fn(),
+ deleteOne: jest.fn()
+ };
+
+ mockFilesCollection = {
+ find: jest.fn().mockReturnValue(mockFilesCursor)
+ };
+
+ dbConn = {
+ collection: jest.fn((name) => {
+ if (name === 'images') {
+ return mockImagesCollection;
+ } else if (name === 'files') {
+ return mockFilesCollection;
+ }
+ })
+ };
+
+ db = {
+ connect: jest.fn().mockResolvedValue(),
+ getConnection: jest.fn().mockReturnValue(dbConn)
+ };
+
+ images = new Images(db);
+ });
+
+ describe('upload', () => {
+ it('should upload an image and return the inserted ID', async () => {
+ const testFile = { originalname: 'test.png', buffer: Buffer.from('dummydata'), mimetype: 'image/png' };
+ const userId = 'user123';
+
+ const result = await images.upload(testFile, userId);
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.getConnection).toHaveBeenCalled();
+ expect(dbConn.collection).toHaveBeenCalledWith('images');
+ expect(mockImagesCollection.insertOne).toHaveBeenCalledWith({
+ userId: userId,
+ file_name: 'test.png',
+ file_content: testFile.buffer.toString('base64'),
+ mime_type: 'image/png',
+ created_at: expect.any(Date)
+ });
+ expect(result).toBe('image123');
+ });
+ });
+
+ describe('get', () => {
+ it('should retrieve the image if found', async () => {
+ const imageId = '65d9a8f9b5e8d1a5e6a8c9f0';
+ const testImage = {
+ file_name: 'test.png',
+ file_content: Buffer.from('dummydata').toString('base64'),
+ mime_type: 'image/png'
+ };
+ mockImagesCollection.findOne.mockResolvedValue(testImage);
+
+ const result = await images.get(imageId);
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.getConnection).toHaveBeenCalled();
+ expect(dbConn.collection).toHaveBeenCalledWith('images');
+ expect(mockImagesCollection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromHexString(imageId) });
+ expect(result).toEqual({
+ file_name: 'test.png',
+ file_content: Buffer.from('dummydata'),
+ mime_type: 'image/png'
+ });
+ });
+
+ it('should return null if image is not found', async () => {
+ const imageId = '65d9a8f9b5e8d1a5e6a8c9f0';
+ mockImagesCollection.findOne.mockResolvedValue(null);
+
+ const result = await images.get(imageId);
+
+ expect(result).toBeNull();
+ });
+ });
+
+ describe('getImages', () => {
+ it('should retrieve a paginated list of images', async () => {
+ const mockImages = [
+ { _id: '1', userId: 'user1', file_name: 'image1.png', file_content: Buffer.from('data1'), mime_type: 'image/png' },
+ { _id: '2', userId: 'user2', file_name: 'image2.png', file_content: Buffer.from('data2'), mime_type: 'image/png' }
+ ];
+
+ mockImagesCollection.countDocuments.mockResolvedValue(2);
+ // Create a mock cursor for images collection
+ const mockFindCursor = {
+ sort: jest.fn().mockReturnThis(),
+ skip: jest.fn().mockReturnThis(),
+ limit: jest.fn().mockReturnThis(),
+ toArray: jest.fn().mockResolvedValue(mockImages), // Return mock images when toArray is called
+ };
+
+ // Mock the find method to return the mock cursor
+ mockImagesCollection.find.mockReturnValue(mockFindCursor);
+ const result = await images.getImages(1, 10);
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.getConnection).toHaveBeenCalled();
+ expect(dbConn.collection).toHaveBeenCalledWith('images');
+ expect(mockImagesCollection.find).toHaveBeenCalledWith({});
+ expect(mockFindCursor.sort).toHaveBeenCalledWith({ created_at: 1 });
+ expect(mockFindCursor.skip).toHaveBeenCalledWith(0);
+ expect(mockFindCursor.limit).toHaveBeenCalledWith(10);
+ expect(result).toEqual({
+ images: [
+ { id: '1', user: 'user1', file_name: 'image1.png', file_content: 'ZGF0YTE=', mime_type: 'image/png' },
+ { id: '2', user: 'user2', file_name: 'image2.png', file_content: 'ZGF0YTI=', mime_type: 'image/png' }
+ ],
+ total: 2,
+ });
+ });
+
+ it('should return an empty array if no images are found', async () => {
+ mockImagesCollection.countDocuments.mockResolvedValue(0);
+
+ const result = await images.getImages(1, 10);
+
+ expect(result).toEqual({ images: [], total: 0 });
+ });
+ });
+
+ describe('getUserImages', () => {
+ it('should return empty images array when no images exist', async () => {
+ mockImagesCollection.countDocuments.mockResolvedValue(0);
+
+ const result = await images.getUserImages(1, 10, 'user123');
+
+ expect(result).toEqual({ images: [], total: 0 });
+ expect(db.connect).toHaveBeenCalled();
+ expect(mockImagesCollection.countDocuments).toHaveBeenCalledWith({ userId: 'user123' });
+ });
+
+ it('should return images when they exist', async () => {
+ const mockImages = [
+ {
+ _id: 'img1',
+ userId: 'user123',
+ file_name: 'image1.png',
+ file_content: Buffer.from('testdata'),
+ mime_type: 'image/png'
+ }
+ ];
+
+ mockImagesCollection.countDocuments.mockResolvedValue(1);
+ mockImagesCollection.find.mockReturnValue({
+ sort: jest.fn().mockReturnThis(),
+ skip: jest.fn().mockReturnThis(),
+ limit: jest.fn().mockReturnThis(),
+ toArray: jest.fn().mockResolvedValue(mockImages)
+ });
+
+ const result = await images.getUserImages(1, 10, 'user123');
+
+ expect(result).toEqual({
+ images: [
+ {
+ id: 'img1',
+ user: 'user123',
+ file_name: 'image1.png',
+ file_content: Buffer.from('testdata').toString('base64'),
+ mime_type: 'image/png'
+ }
+ ],
+ total: 1
+ });
+ expect(db.connect).toHaveBeenCalled();
+ expect(mockImagesCollection.countDocuments).toHaveBeenCalledWith({ userId: 'user123' });
+ });
+ });
+ describe('delete', () => {
+ it('should not delete the image when it exists in the files collection', async () => {
+ const uid = 'user123';
+ const imgId = '507f191e810c19729de860ea'; // A valid 24-character ObjectId string
+
+ // Mock the files collection cursor to simulate an image found
+ const mockFilesCursor = {
+ toArray: jest.fn().mockResolvedValue([{ _id: imgId }]) // Image found
+ };
+
+ mockFilesCollection.find.mockReturnValue(mockFilesCursor);
+ mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 0 });
+
+ const result = await images.delete(uid, imgId);
+
+ // Ensure the files collection is queried
+ expect(dbConn.collection).toHaveBeenCalledWith('files');
+ expect(mockFilesCollection.find).toHaveBeenCalledWith({
+ userId: uid,
+ content: { $regex: new RegExp(`/api/image/get/${imgId}`) },
+ });
+
+ // Ensure the images collection is queried for deletion
+ expect(dbConn.collection).toHaveBeenCalledWith('files');
+ expect(mockImagesCollection.deleteOne).not.toHaveBeenCalledWith({
+ _id: ObjectId.createFromHexString(imgId), // Ensure the ObjectId is created correctly
+ });
+
+ expect(result).toEqual({ deleted: false });
+ });
+
+ it('should delete the image if not found in the files collection', async () => {
+ const uid = 'user123';
+ const imgId = '507f191e810c19729de860ea';
+
+ // Mock the files collection cursor to simulate the image not being found
+ const mockFindCursor = {
+ toArray: jest.fn().mockResolvedValue([]) // Empty array means image not found
+ };
+
+ mockFilesCollection.find.mockReturnValue(mockFindCursor);
+ mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 1 });
+
+ const result = await images.delete(uid, imgId);
+
+ // Ensure the deleteOne is not called if the image is not found
+ expect(mockImagesCollection.deleteOne).toHaveBeenCalled();
+ expect(result).toEqual({ deleted: true });
+ });
+
+ it('should return false if the delete operation fails in the images collection', async () => {
+ const uid = 'user123';
+ const imgId = '507f191e810c19729de860ea';
+
+ // Mock the files collection cursor to simulate the image being found
+ const mockFindCursor = {
+ toArray: jest.fn().mockResolvedValue([{ _id: imgId }]) // Image found
+ };
+
+ mockFilesCollection.find.mockReturnValue(mockFindCursor);
+ mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 0 }); // Simulate failure
+
+ const result = await images.delete(uid, imgId);
+
+ // Ensure the images collection deletion is called
+ expect(mockImagesCollection.deleteOne).not.toHaveBeenCalledWith({
+ _id: ObjectId.createFromHexString(imgId), // Ensure the ObjectId is created correctly
+ });
+
+ expect(result).toEqual({ deleted: false });
+ });
+
+ });
+});
\ No newline at end of file
diff --git a/server/controllers/images.js b/server/controllers/images.js
index b77ed96..7de02ad 100644
--- a/server/controllers/images.js
+++ b/server/controllers/images.js
@@ -50,6 +50,58 @@ class ImagesController {
}
};
+ getImages = async (req, res, next) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = parseInt(req.query.limit) || 5;
+ const images = await this.images.getImages(page, limit);
+
+ if (!images || images.length === 0) {
+ throw new AppError(IMAGE_NOT_FOUND);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.status(200).json(images);
+ } catch (error) {
+ return next(error);
+ }
+ };
+
+ getUserImages = async (req, res, next) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = parseInt(req.query.limit) || 5;
+ const uid = req.query.uid;
+ const images = await this.images.getUserImages(page, limit, uid);
+
+ if (!images || images.length === 0) {
+ throw new AppError(IMAGE_NOT_FOUND);
+ }
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.status(200).json(images);
+ } catch (error) {
+ return next(error);
+ }
+ };
+
+ delete = async (req, res, next) => {
+ try {
+ const uid = req.query.uid;
+ const imgId = req.query.imgId;
+
+ if (!uid || !imgId) {
+ throw new AppError(MISSING_REQUIRED_PARAMETER);
+ }
+ const images = await this.images.delete(uid, imgId);
+
+ res.setHeader('Content-Type', 'application/json');
+ return res.status(200).json(images);
+ } catch (error) {
+ return next(error);
+ }
+ };
+
}
module.exports = ImagesController;
diff --git a/server/models/images.js b/server/models/images.js
index 67a6583..16e52de 100644
--- a/server/models/images.js
+++ b/server/models/images.js
@@ -42,6 +42,84 @@ class Images {
};
}
+ async getImages(page, limit) {
+ await this.db.connect()
+ const conn = this.db.getConnection();
+
+ const imagesCollection = conn.collection('images');
+
+
+ const total = await imagesCollection.countDocuments();
+ if (!total || total === 0) return { images: [], total };
+
+ const result = await imagesCollection.find({})
+ .sort({ created_at: 1 })
+ .skip((page - 1) * limit)
+ .limit(limit)
+ .toArray();
+
+ const objImages = result.map(image => ({
+ id: image._id,
+ user: image.userId,
+ file_name: image.file_name,
+ file_content: image.file_content.toString('base64'),
+ mime_type: image.mime_type
+ }));
+
+ let respObj = {
+ images: objImages,
+ total: total
+ }
+
+ return respObj;
+ }
+
+ async getUserImages(page, limit, uid) {
+ await this.db.connect()
+ const conn = this.db.getConnection();
+ const imagesCollection = conn.collection('images');
+ const total = await imagesCollection.countDocuments({ userId: uid });
+ if (!total || total === 0) return { images: [], total };
+
+ const result = await imagesCollection.find({ userId: uid })
+ .sort({ created_at: 1 })
+ .skip((page - 1) * limit)
+ .limit(limit)
+ .toArray();
+
+ const objImages = result.map(image => ({
+ id: image._id,
+ user: image.userId,
+ file_name: image.file_name,
+ file_content: image.file_content.toString('base64'),
+ mime_type: image.mime_type
+ }));
+
+ let respObj = {
+ images: objImages,
+ total: total
+ }
+
+ return respObj;
+ }
+
+ async delete(uid, imgId) {
+ let resp = false;
+ await this.db.connect()
+ const conn = this.db.getConnection();
+ const quizColl = conn.collection('files');
+ const rgxImg = new RegExp(`/api/image/get/${imgId}`);
+
+ const result = await quizColl.find({ userId: uid, content: { $regex: rgxImg }}).toArray();
+ if(!result || result.length < 1){
+ const imgsColl = conn.collection('images');
+ const isDeleted = await imgsColl.deleteOne({ _id: ObjectId.createFromHexString(imgId) });
+ if(isDeleted){
+ resp = true;
+ }
+ }
+ return { deleted: resp };
+ }
}
module.exports = Images;
diff --git a/server/routers/images.js b/server/routers/images.js
index 06e2830..f2d601a 100644
--- a/server/routers/images.js
+++ b/server/routers/images.js
@@ -12,5 +12,8 @@ const upload = multer({ storage: storage });
router.post("/upload", jwt.authenticate, upload.single('image'), asyncHandler(images.upload));
router.get("/get/:id", asyncHandler(images.get));
+router.get("/getImages", asyncHandler(images.getImages));
+router.get("/getUserImages", asyncHandler(images.getUserImages));
+router.delete("/delete", asyncHandler(images.delete));
module.exports = router;