diff --git a/client/package-lock.json b/client/package-lock.json index 1ea1d6c..2ef0628 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,6 +14,7 @@ "axios": "^1.6.8", "bootstrap": "^5.3.3", "js-cookie": "^3.0.5", + "jsonwebtoken": "^9.0.2", "jspdf": "^2.5.2", "jspdf-autotable": "^3.8.4", "md5": "^2.3.0", @@ -6091,6 +6092,11 @@ "node": ">= 0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -7528,6 +7534,14 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -12694,6 +12708,27 @@ "node": ">=0.10.0" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jspdf": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", @@ -12746,6 +12781,25 @@ "pako": "~0.2.5" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -12889,6 +12943,36 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -12899,6 +12983,11 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", diff --git a/client/package.json b/client/package.json index 486512c..1bb97b1 100644 --- a/client/package.json +++ b/client/package.json @@ -9,6 +9,7 @@ "axios": "^1.6.8", "bootstrap": "^5.3.3", "js-cookie": "^3.0.5", + "jsonwebtoken": "^9.0.2", "jspdf": "^2.5.2", "jspdf-autotable": "^3.8.4", "md5": "^2.3.0", diff --git a/client/src/App.js b/client/src/App.js index 5fa4542..113d7c9 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -17,6 +17,7 @@ import TokenRefresher from "./components/TokenRefresher"; import DepartmentConsolidated from "./Pages/DepartmentConsolidated"; import PanelConsolidated from "./Pages/PanelConsolidated"; import { AdminFacultyPage } from "./Pages/AdminFacultyPage"; +import AdminCoursePage from "./Pages/AdminCoursePage"; function App() { @@ -37,6 +38,7 @@ function App() { } />} /> } />} /> } />} /> + } />} /> ); diff --git a/client/src/Pages/AdminCoursePage.jsx b/client/src/Pages/AdminCoursePage.jsx new file mode 100644 index 0000000..f7a4eea --- /dev/null +++ b/client/src/Pages/AdminCoursePage.jsx @@ -0,0 +1,313 @@ +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; +import Navbar from './Navbar'; +import { toast, ToastContainer } from "react-toastify"; +import 'react-toastify/dist/ReactToastify.css'; +import { useNavigate } from 'react-router-dom'; + +export const AdminCoursePage = () => { + const [courses, setCourses] = useState([]); + const [showForm, setShowForm] = useState(false); + const [isEditMode, setIsEditMode] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [currentCourse, setCurrentCourse] = useState({ + courseId: '', + name: '', + department: '', + program: '', + scheme: '', + semester: '', + status: 'not submitted', + }); + const navigate = useNavigate(); + + // Fetch all courses + useEffect(() => { + const checkAdmin = async () => { + try { + const res = await axios.get("http://localhost:8080/api/me", { withCredentials: true }); + if (!res.data.isAdmin) { + handleUnauthorizedAccess(); + } else { + setIsLoading(false); + } + } catch (error) { + console.error("Unauthorized access:", error); + handleUnauthorizedAccess(); + } + }; + fetchCourses(); + checkAdmin(); + }, []); + + const handleUnauthorizedAccess = async () => { + try { + toast.warning("Unauthorized access. Logging out...", { position: "top-center" }); + + // Attempt to log out + await axios.get("http://localhost:8080/auth/logout", { withCredentials: true }); + + // Delay redirection to show the toast message + setTimeout(() => { + navigate("/"); // Redirect to login + }, 1500); + } catch (error) { + console.error("Error during unauthorized access handling:", error); + } + }; + + + // Fetch all courses after confirming admin status + useEffect(() => { + if (!isLoading) { + fetchCourses(); + } + }, [isLoading]); + + const fetchCourses = async () => { + try { + const res = await axios.get('http://localhost:8080/api/courses'); + setCourses(res.data); + } catch (error) { + console.error("Error fetching courses: ", error); + } + }; + + // Add or Update Course + const handleAddOrUpdateCourse = async (e) => { + e.preventDefault(); + + try { + if (isEditMode) { + await axios.put(`http://localhost:8080/api/courses/${currentCourse.courseId}`, currentCourse); + toast.success('Course updated successfully!', { position: 'top-center' }); + } else { + await axios.post('http://localhost:8080/api/courses', currentCourse); + toast.success('Course added successfully!', { position: 'top-center' }); + } + resetForm(); + fetchCourses(); + } catch (error) { + console.error("Error adding/updating course:", error); + toast.error("An error occurred!", { position: 'top-center' }); + } + }; + + // Delete Course + const handleDeleteCourse = async (courseId) => { + const confirm = window.confirm('⚠️ Are you sure you want to delete this course?'); + if (confirm) { + try { + await axios.delete(`http://localhost:8080/api/courses/${courseId}`); + fetchCourses(); + toast.success('Course deleted successfully!', { position: 'top-center' }); + } catch (error) { + console.error("Error deleting course:", error); + toast.error("An error occurred!", { position: 'top-center' }); + } + } + }; + + // Open Form for Edit + const handleOpenEditForm = (course) => { + setCurrentCourse(course); + setIsEditMode(true); + setShowForm(true); + }; + + // Open Form for Add + const handleOpenAddForm = () => { + resetForm(); + setIsEditMode(false); + setShowForm(true); + }; + + // Reset form + const resetForm = () => { + setCurrentCourse({ + courseId: '', + name: '', + department: '', + program: '', + scheme: '', + semester: '', + status: 'not submitted', + }); + setShowForm(false); + }; + + const filteredCourses = courses.filter((course) => + course.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + if (isLoading) { + return

Loading...

; + } + + return ( + <> + + +
+

Course Management

+ +
+ setSearchQuery(e.target.value)} + style={inputStyle} + /> + +
+
+ + {showForm && ( +
+

{isEditMode ? 'Edit Course' : 'Add New Course'}

+
+ setCurrentCourse({ ...currentCourse, courseId: e.target.value })} + style={inputStyle} + /> + setCurrentCourse({ ...currentCourse, name: e.target.value })} + style={inputStyle} + /> + setCurrentCourse({ ...currentCourse, department: e.target.value })} + style={inputStyle} + /> + setCurrentCourse({ ...currentCourse, program: e.target.value })} + style={inputStyle} + /> + setCurrentCourse({ ...currentCourse, scheme: e.target.value })} + style={inputStyle} + /> + setCurrentCourse({ ...currentCourse, semester: e.target.value })} + style={inputStyle} + /> + + +
+ + +
+
+
+ )} + + {/* TABLE DATA */} + + + + + + + + + + + + + + + {filteredCourses.length > 0 ? ( + filteredCourses.map((c) => ( + + + + + + + + + + + )) + ) : ( + + + + )} + +
Course IDNameDepartmentProgramSchemeSemesterStatusActions
{c.courseId}{c.name}{c.department}{c.program}{c.scheme}{c.semester}{c.status} + + +
+ No courses found +
+ + ); +}; + +const tableStyle = { + width: '100%', + borderCollapse: 'collapse', + marginTop: '20px', +}; + +const inputStyle = { + padding: '10px', + width: '250px', + borderRadius: '5px', + border: '1px solid #ddd', + fontSize: '16px', + marginRight: '10px', +}; + +const formStyle = { + backgroundColor: '#fff', + padding: '20px', + borderRadius: '10px', + maxWidth: '500px', + margin: '0 auto', + boxShadow: '0 5px 20px rgba(0,0,0,0.1)', +}; + +const addButtonStyle = { + backgroundColor: '#4CAF50', + color: '#fff', + padding: '10px 15px', + borderRadius: '5px', + border: 'none', + cursor: 'pointer', +}; + +const submitButtonStyle = { backgroundColor: '#4CAF50', color: 'white', padding: '10px 20px', borderRadius: '5px' }; +const cancelButtonStyle = { backgroundColor: '#f44336', color: 'white', padding: '10px 20px', borderRadius: '5px' }; +const editButtonStyle = { backgroundColor: '#4CAF50', color: 'white', padding: '5px 10px', borderRadius: '5px' }; +const deleteButtonStyle = { backgroundColor: '#f44336', color: 'white', padding: '5px 10px', borderRadius: '5px' }; + +export default AdminCoursePage; diff --git a/client/src/Pages/AdminFacultyPage.jsx b/client/src/Pages/AdminFacultyPage.jsx index c64b348..98fa3b9 100644 --- a/client/src/Pages/AdminFacultyPage.jsx +++ b/client/src/Pages/AdminFacultyPage.jsx @@ -3,6 +3,7 @@ import axios from 'axios'; import Navbar from './Navbar'; import { toast, ToastContainer } from "react-toastify"; import 'react-toastify/dist/ReactToastify.css'; +import { useNavigate } from 'react-router-dom'; export const AdminFacultyPage = () => { @@ -10,6 +11,7 @@ export const AdminFacultyPage = () => { const [showForm, setShowForm] = useState(false); const [isEditMode, setIsEditMode] = useState(false); const [searchQuery, setSearchQuery] = useState(''); + const [isLoading, setIsLoading] = useState(true); const [currentFaculty, setCurrentFaculty] = useState({ facultyId: '', name: '', @@ -19,13 +21,51 @@ export const AdminFacultyPage = () => { courses: [''], }); const [courses, setcourses] = useState({}); + const navigate = useNavigate(); // Fetch all faculties useEffect(() => { + const checkAdmin = async () => { + try { + const res = await axios.get("http://localhost:8080/api/me", { withCredentials: true }); + console.log(res.data); + if (!res.data.isAdmin) { + handleUnauthorizedAccess(); + } else { + setIsLoading(false); + } + } catch (error) { + console.error("Unauthorized access:", error); + handleUnauthorizedAccess(); + } + }; fetchFaculties(); fetchCourses(); + checkAdmin(); }, []); + const handleUnauthorizedAccess = async () => { + try { + toast.warning("Unauthorized access. Logging out...", { position: "top-center" }); + + // Attempt to log out + await axios.get("http://localhost:8080/auth/logout", { withCredentials: true }); + + // Delay redirection to show the toast message + setTimeout(() => { + navigate("/"); // Redirect to login + }, 1500); + } catch (error) { + console.error("Error during unauthorized access handling:", error); + } +}; + + useEffect(() => { + if (!isLoading) { + fetchCourses(); + } + }, [isLoading]); + const fetchFaculties = async () => { const res = await axios.get('http://localhost:8080/api/faculty'); setFaculties(res.data); @@ -125,6 +165,10 @@ export const AdminFacultyPage = () => { faculty.name.toLowerCase().includes(searchQuery.toLowerCase()) ); + if (isLoading) { + return

Loading...

; + } + return ( <> @@ -211,7 +255,7 @@ export const AdminFacultyPage = () => { /> - + {currentFaculty.courses.map((course, index) => (
diff --git a/client/src/Pages/Navbar.jsx b/client/src/Pages/Navbar.jsx index 18184e2..30d2401 100644 --- a/client/src/Pages/Navbar.jsx +++ b/client/src/Pages/Navbar.jsx @@ -98,6 +98,13 @@ const Navbar = () => { Faculty )} + {isAdmin && ( + e.target.style.backgroundColor = "#660000"} + onMouseLeave={(e) => e.target.style.backgroundColor = "transparent"}> + Courses + + )}