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) => (
+
+
+
+
+ {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;