diff --git a/client/src/App.tsx b/client/src/App.tsx index 6a2d060..fb20f4f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -26,10 +26,15 @@ import Footer from './components/Footer/Footer'; import ApiService from './services/ApiService'; import OAuthCallback from './pages/AuthManager/callback/AuthCallback'; +import Users from './pages/Admin/Users'; +import Images from './pages/Admin/Images'; +import Stats from './pages/Admin/Stats'; + + const App: React.FC = () => { const [isAuthenticated, setIsAuthenticated] = useState(ApiService.isLoggedIn()); const [isTeacherAuthenticated, setIsTeacherAuthenticated] = useState(ApiService.isLoggedInTeacher()); - const [isAdmin, setIsAdmin] = useState(false); + //const [isAdmin, setIsAdmin] = useState(false); const [isRoomRequireAuthentication, setRoomsRequireAuth] = useState(null); const location = useLocation(); @@ -100,6 +105,11 @@ const App: React.FC = () => { {/* Pages authentification sélection */} } /> + + } /> + } /> + } /> + diff --git a/client/src/Types/ImageType.tsx b/client/src/Types/ImageType.tsx new file mode 100644 index 0000000..d0a1541 --- /dev/null +++ b/client/src/Types/ImageType.tsx @@ -0,0 +1,17 @@ +export interface ImageType { + id: string; + file_content: string; + file_name: string; + mime_type: string; +} + +export interface ImagesResponse { + images: ImageType[]; + total: number; +} + +export interface ImagesParams { + page: number; + limit: number; + uid?: string; +} \ No newline at end of file diff --git a/client/src/Types/QuizType.tsx b/client/src/Types/QuizType.tsx index b5e2b08..5c2eb0d 100644 --- a/client/src/Types/QuizType.tsx +++ b/client/src/Types/QuizType.tsx @@ -9,3 +9,11 @@ export interface QuizType { created_at: Date; updated_at: Date; } + +export interface QuizTypeShort { + _id: string; + userId: string; + title: string; + created_at: Date; + updated_at: Date; +} \ No newline at end of file diff --git a/client/src/Types/UserType.tsx b/client/src/Types/UserType.tsx new file mode 100644 index 0000000..db6336d --- /dev/null +++ b/client/src/Types/UserType.tsx @@ -0,0 +1,11 @@ +export interface UserType { + id: string; + name: string; + email: string; + created_at: string; + roles: string[]; +} + +export interface UsersResponse { + users: UserType[]; +} \ No newline at end of file diff --git a/client/src/components/AdminDrawer/AdminDrawer.tsx b/client/src/components/AdminDrawer/AdminDrawer.tsx index d61df5e..900634d 100644 --- a/client/src/components/AdminDrawer/AdminDrawer.tsx +++ b/client/src/components/AdminDrawer/AdminDrawer.tsx @@ -10,6 +10,7 @@ import ListItemText from '@mui/material/ListItemText'; import BarChartIcon from '@mui/icons-material/BarChart'; import ImageIcon from '@mui/icons-material/Image'; import PeopleIcon from '@mui/icons-material/People'; +import { useNavigate } from 'react-router-dom'; const styles = { drawerBg: 'rgba(82, 113, 255, 0.85)', @@ -21,23 +22,29 @@ const styles = { export default function AdminDrawer() { const [open, setOpen] = React.useState(false); + const navigate = useNavigate(); const toggleDrawer = (isOpen: boolean) => () => { setOpen(isOpen); }; + const handleNavigation = (path: string) => { + navigate(path); + setOpen(false); + }; + const menuItems = [ - { text: 'Stats', icon: }, - { text: 'Images', icon: }, - { text: 'Users', icon: }, + { text: 'Stats', icon: , path: '/admin/stats' }, + { text: 'Images', icon: , path: '/admin/images' }, + { text: 'Users', icon: , path: '/admin/users' }, ]; const list = ( - {menuItems.map(({ text, icon }) => ( + {menuItems.map(({ text, icon, path }) => ( - + handleNavigation(path)}> {icon} diff --git a/client/src/pages/Admin/Images.tsx b/client/src/pages/Admin/Images.tsx new file mode 100644 index 0000000..ad6326e --- /dev/null +++ b/client/src/pages/Admin/Images.tsx @@ -0,0 +1,114 @@ +import React, { useState, useEffect } from "react"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableRow, + TableHead, + IconButton, + Paper, + Box, + CircularProgress, + Button, + Typography +} from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { ImageType } from "../../Types/ImageType"; +import ApiService from '../../services/ApiService'; + +const Images: React.FC = () => { + const [images, setImages] = useState([]); + const [totalImg, setTotalImg] = useState(0); + const [imgPage, setImgPage] = useState(1); + const [imgLimit] = useState(10); + const [loading, setLoading] = useState(false); + + const fetchImages = async (page: number, limit: number) => { + setLoading(true); + const data = await ApiService.getImages(page, limit); + setImages(data.images); + setTotalImg(data.total); + setLoading(false); + }; + + useEffect(() => { + fetchImages(imgPage, imgLimit); + }, [imgPage]); + + + const handleDelete = async (id: string) => { + setLoading(true); + const isDeleted = await ApiService.deleteImage(id); + setLoading(false); + if (isDeleted) { + setImages(images.filter(image => image.id !== id)); + } + }; + + const handleNextPage = () => { + if ((imgPage * imgLimit) < totalImg) { + setImgPage(prev => prev + 1); + } + }; + + const handlePrevPage = () => { + if (imgPage > 1) { + setImgPage(prev => prev - 1); + } + }; + + return ( + + {loading ? ( + + + + ) : ( + + + + + + Nom + Image ID + + + + {images.map((obj: ImageType) => ( + + + {`Image + + {obj.file_name} + + {obj.id} + + + handleDelete(obj.id)} color="error"> + + + + + ))} + +
+
+ )} + + + + +
+ ); +}; + +export default Images; diff --git a/client/src/pages/Admin/Stats.tsx b/client/src/pages/Admin/Stats.tsx new file mode 100644 index 0000000..6c42b70 --- /dev/null +++ b/client/src/pages/Admin/Stats.tsx @@ -0,0 +1,57 @@ +import React, { useState, useEffect } from "react"; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import ApiService from '../../services/ApiService'; +import { QuizTypeShort } from "../../Types/QuizType"; + +const Users: React.FC = () => { + const [quizzes, setQuizzes] = useState([]); + + useEffect(() => { + const fetchUsers = async () => { + try { + const data = await ApiService.getQuizzes(); + setQuizzes(data); + } catch (error) { + console.error("Error fetching quizzes:", error); + } + }; + fetchUsers(); + }, []); + + const handleDelete = (id: string) => { + setQuizzes(quizzes.filter(quiz => quiz._id !== id)); + }; + + return ( + + + + + Enseignant + Titre + Crée + Modifié + + + + {quizzes.map((quiz) => ( + + {quiz.userId} + {quiz.title} + {new Date(quiz.created_at).toLocaleDateString()} + {new Date(quiz.updated_at).toLocaleDateString()} + + handleDelete(quiz._id)} color="error"> + + + + + ))} + +
+
+ ); +}; + +export default Users; diff --git a/client/src/pages/Admin/Users.tsx b/client/src/pages/Admin/Users.tsx new file mode 100644 index 0000000..059f4c0 --- /dev/null +++ b/client/src/pages/Admin/Users.tsx @@ -0,0 +1,57 @@ +import React, { useState, useEffect } from "react"; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import ApiService from '../../services/ApiService'; +import { UserType } from "../../Types/UserType"; + +const Users: React.FC = () => { + const [users, setUsers] = useState([]); + + useEffect(() => { + const fetchUsers = async () => { + try { + const data = await ApiService.getUsers(); + setUsers(data); + } catch (error) { + console.error("Error fetching users:", error); + } + }; + fetchUsers(); + }, []); + + const handleDelete = (email: string) => { + setUsers(users.filter(user => user.email !== email)); + }; + + return ( + + + + + Nom + Courriel + Crée + Roles + + + + {users.map((user) => ( + + {user.name} + {user.email} + {new Date(user.created_at).toLocaleDateString()} + {user.roles?.join(", ")} + + handleDelete(user.email)} color="error"> + + + + + ))} + +
+
+ ); +}; + +export default Users; diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index b13a369..0991bc5 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -3,8 +3,10 @@ import { jwtDecode } from 'jwt-decode'; import { ENV_VARIABLES } from '../constants'; import { FolderType } from 'src/Types/FolderType'; -import { QuizType } from 'src/Types/QuizType'; +import { QuizType, QuizTypeShort } from 'src/Types/QuizType'; import { RoomType } from 'src/Types/RoomType'; +import { UserType } from 'src/Types/UserType'; +import { ImagesResponse, ImagesParams } from 'src/Types/ImageType'; type ApiResponse = boolean | string; @@ -1009,6 +1011,7 @@ public async login(email: string, password: string): Promise { return `Une erreur inattendue s'est produite.`; } } + public async getRoomTitle(roomId: string): Promise { try { if (!roomId) { @@ -1167,8 +1170,120 @@ public async login(email: string, password: string): Promise { return `ERROR : Une erreur inattendue s'est produite.` } } - // NOTE : Get Image pas necessaire + public async getUsers(): Promise { + try { + + const url: string = this.constructRequestUrl(`/admin/getUsers`); + const headers = this.constructRequestHeaders(); + const result: AxiosResponse = await axios.get(url, { headers }); + + if (result.status !== 200) { + throw new Error(`L'obtention des titres des salles a échoué. Status: ${result.status}`); + } + console.log(result.data); + + return result.data.users; + } 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 getImages(page: number, limit: number): Promise { + try { + const url: string = this.constructRequestUrl(`/admin/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}`); + } + console.log(result.data); + const images = result.data.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(`/admin/deleteImage`); + const headers = this.constructRequestHeaders(); + let params = { 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.`); + } + } + + public async getQuizzes(): Promise { + try { + const url: string = this.constructRequestUrl(`/admin/getQuizzes`); + const headers = this.constructRequestHeaders(); + const result: AxiosResponse = await axios.get(url, { headers }); + + if (result.status !== 200) { + throw new Error(`L'affichage des images a échoué. Status: ${result.status}`); + } + const quiz = result.data.quizzes; + + return quiz; + + } 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.`); + } + } + } const apiService = new ApiService(); diff --git a/server/controllers/admin.js b/server/controllers/admin.js index fb7071c..6487901 100644 --- a/server/controllers/admin.js +++ b/server/controllers/admin.js @@ -36,7 +36,7 @@ class AdminController { const imgs = await this.model.getImages(page, limit); - return res.status(200).json({ imgs }); + return res.status(200).json({ data: imgs }); } catch (error) { return next(error); } diff --git a/server/models/admin.js b/server/models/admin.js index a7634d7..df4b8f1 100644 --- a/server/models/admin.js +++ b/server/models/admin.js @@ -38,27 +38,13 @@ class Admin { const conn = this.db.getConnection(); const quizColl = conn.collection('files'); - - const result = await quizColl.find({}).toArray(); + const projection = { content: 0, folderName: 0, folderId: 0 }; + const result = await quizColl.find({}, projection).toArray(); if (!result) return null; return result; } - - async deleteQuiz(id) { - let deleted = false; - await this.db.connect() - const conn = this.db.getConnection(); - - const quizColl = conn.collection('files'); - - const result = await quizColl.deleteOne({ _id: ObjectId.createFromHexString(id) }); - - if (result) deleted = true; - - return deleted; - } async getImages(page, limit) { await this.db.connect() diff --git a/server/routers/admin.js b/server/routers/admin.js index b15aa07..3f547da 100644 --- a/server/routers/admin.js +++ b/server/routers/admin.js @@ -10,7 +10,6 @@ router.get("/getUsers", asyncHandler(admin.getUsers)); router.get("/getQuizzes", asyncHandler(admin.getQuizzes)); router.get("/getImages", asyncHandler(admin.getImages)); router.delete("/deleteUser", asyncHandler(admin.deleteUser)); -router.delete("/deleteQuiz", asyncHandler(admin.deleteQuiz)); router.delete("/deleteImage", jwt.authenticate, asyncHandler(admin.deleteImage)); module.exports = router;