From f882f2cf56a6d64e8831644fc895d0683ad534e1 Mon Sep 17 00:00:00 2001 From: MarvsTech Date: Wed, 11 Sep 2024 19:33:10 -0800 Subject: [PATCH] Initial commit for aws_deployment --- client/.env | 1 + client/.gitignore | 23 ++ client/package.json | 42 ++++ client/public/favicon.ico | Bin 0 -> 3870 bytes client/public/index.html | 43 ++++ client/public/logo192.png | Bin 0 -> 5347 bytes client/public/logo512.png | Bin 0 -> 9664 bytes client/public/manifest.json | 25 ++ client/public/robots.txt | 3 + client/src/App.css | 9 + client/src/App.js | 101 ++++++++ client/src/UserContext.js | 11 + client/src/components/AdminView.js | 60 +++++ client/src/components/AppNavbar.js | 51 +++++ client/src/components/ArchiveCourse.js | 87 +++++++ client/src/components/Banner.js | 18 ++ client/src/components/CourseCard.js | 56 +++++ client/src/components/CourseSearch.js | 50 ++++ client/src/components/EditCourse.js | 135 +++++++++++ client/src/components/FeaturedCourses.js | 51 +++++ client/src/components/Highlights.js | 44 ++++ client/src/components/PreviewCourses.js | 26 +++ client/src/components/ResetPassword.js | 80 +++++++ client/src/components/SearchByPrice.js | 77 +++++++ client/src/components/UpdateProfile.js | 90 ++++++++ client/src/components/UserView.js | 33 +++ client/src/data/coursesData.js | 25 ++ client/src/index.js | 29 +++ client/src/pages/AddCourse.js | 101 ++++++++ client/src/pages/CourseView.js | 105 +++++++++ client/src/pages/Courses.js | 74 ++++++ client/src/pages/Error.js | 15 ++ client/src/pages/Home.js | 23 ++ client/src/pages/Login.js | 152 ++++++++++++ client/src/pages/Logout.js | 29 +++ client/src/pages/Profile.js | 71 ++++++ client/src/pages/Register.js | 164 +++++++++++++ server/.env | 2 + server/.gitignore | 1 + server/auth.js | 98 ++++++++ server/controllers/course.js | 245 ++++++++++++++++++++ server/controllers/user.js | 279 +++++++++++++++++++++++ server/index.js | 45 ++++ server/models/Course.js | 41 ++++ server/models/User.js | 49 ++++ server/package.json | 23 ++ server/routes/course.js | 56 +++++ server/routes/user.js | 71 ++++++ 48 files changed, 2814 insertions(+) create mode 100644 client/.env create mode 100644 client/.gitignore create mode 100644 client/package.json create mode 100644 client/public/favicon.ico create mode 100644 client/public/index.html create mode 100644 client/public/logo192.png create mode 100644 client/public/logo512.png create mode 100644 client/public/manifest.json create mode 100644 client/public/robots.txt create mode 100644 client/src/App.css create mode 100644 client/src/App.js create mode 100644 client/src/UserContext.js create mode 100644 client/src/components/AdminView.js create mode 100644 client/src/components/AppNavbar.js create mode 100644 client/src/components/ArchiveCourse.js create mode 100644 client/src/components/Banner.js create mode 100644 client/src/components/CourseCard.js create mode 100644 client/src/components/CourseSearch.js create mode 100644 client/src/components/EditCourse.js create mode 100644 client/src/components/FeaturedCourses.js create mode 100644 client/src/components/Highlights.js create mode 100644 client/src/components/PreviewCourses.js create mode 100644 client/src/components/ResetPassword.js create mode 100644 client/src/components/SearchByPrice.js create mode 100644 client/src/components/UpdateProfile.js create mode 100644 client/src/components/UserView.js create mode 100644 client/src/data/coursesData.js create mode 100644 client/src/index.js create mode 100644 client/src/pages/AddCourse.js create mode 100644 client/src/pages/CourseView.js create mode 100644 client/src/pages/Courses.js create mode 100644 client/src/pages/Error.js create mode 100644 client/src/pages/Home.js create mode 100644 client/src/pages/Login.js create mode 100644 client/src/pages/Logout.js create mode 100644 client/src/pages/Profile.js create mode 100644 client/src/pages/Register.js create mode 100644 server/.env create mode 100644 server/.gitignore create mode 100644 server/auth.js create mode 100644 server/controllers/course.js create mode 100644 server/controllers/user.js create mode 100644 server/index.js create mode 100644 server/models/Course.js create mode 100644 server/models/User.js create mode 100644 server/package.json create mode 100644 server/routes/course.js create mode 100644 server/routes/user.js diff --git a/client/.env b/client/.env new file mode 100644 index 0000000..0d5ad85 --- /dev/null +++ b/client/.env @@ -0,0 +1 @@ +REACT_APP_API_URL=http://:4000 \ No newline at end of file diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..4d29575 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..006bb4f --- /dev/null +++ b/client/package.json @@ -0,0 +1,42 @@ +{ + "name": "s54", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "bootstrap": "^5.3.0", + "react": "^18.2.0", + "react-bootstrap": "^2.7.4", + "react-dom": "^18.2.0", + "react-router-dom": "^6.13.0", + "react-scripts": "5.0.1", + "sweetalert2": "^11.7.12", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/client/public/favicon.ico b/client/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/client/public/index.html b/client/public/index.html new file mode 100644 index 0000000..aa069f2 --- /dev/null +++ b/client/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/client/public/logo192.png b/client/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/client/public/manifest.json b/client/public/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/client/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/client/public/robots.txt b/client/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/client/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/client/src/App.css b/client/src/App.css new file mode 100644 index 0000000..54f9e72 --- /dev/null +++ b/client/src/App.css @@ -0,0 +1,9 @@ +*{ + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.cardHighlight { + min-height: 100%; +} \ No newline at end of file diff --git a/client/src/App.js b/client/src/App.js new file mode 100644 index 0000000..c637f54 --- /dev/null +++ b/client/src/App.js @@ -0,0 +1,101 @@ +import { useState, useEffect } from 'react'; +import { Container } from 'react-bootstrap'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom'; +import AppNavbar from './components/AppNavbar'; +// import Banner from './components/Banner'; +// import Highlights from './components/Highlights'; +import Courses from './pages/Courses'; +import CourseView from './pages/CourseView'; +import Error from './pages/Error'; +import Home from './pages/Home'; +import Login from './pages/Login'; +import Logout from './pages/Logout'; +import Register from './pages/Register'; +import AddCourse from './pages/AddCourse'; +import Profile from './pages/Profile'; +import './App.css'; +import { UserProvider } from './UserContext'; + +// React JS is a single page application (SPA) +// Whenever a link is clicked, it functions as if the page is being reloaded but what it actually does is it goes through the process of rendering, mounting, rerendering and unmounting components +// When a link is clicked, React JS changes the url of the application to mirror how HTML accesses its urls +// It renders the component executing the function component and it's expressions +// After rendering it mounts the component displaying the elements +// Whenever a state is updated or changes are made with React JS, it rerenders the component +// Lastly, when a different page is loaded, it unmounts the component and repeats this process +// The updating of the user interface closely mirrors that of how HTML deals with page navigation with the exception that React JS does not reload the whole page +function App() { + + // State hook for the user state that's defined here for a global scope + // Initialized as an object with properties from the localStorage + // This will be used to store the user information and will be used for validating if a user is logged in on the app or not + const [user, setUser] = useState({ + id: null, + isAdmin: null + }); + + // Function for clearing localStorage on logout + const unsetUser = () => { + + localStorage.clear(); + + }; + + //Because our user state's values are reset to null every time the user reloads the page (thus logging the user out), we want to use React's useEffect hook to fetch the logged-in user's details when the page is reloaded. By using the token saved in localStorage when a user logs in, we can fetch the their data from the database, and re-set the user state values back to the user's details. + useEffect(() => { + + // console.log(user); + fetch(`${process.env.REACT_APP_API_URL}/users/details`, { + headers: { + Authorization: `Bearer ${ localStorage.getItem('token') }` + } + }) + .then(res => res.json()) + .then(data => { + console.log(data) + // Set the user states values with the user details upon successful login. + if (typeof data._id !== "undefined") { + + setUser({ + id: data._id, + isAdmin: data.isAdmin + }); + + // Else set the user states to the initial values + } else { + + setUser({ + id: null, + isAdmin: null + }); + + } + + }) + + }, []); + + return ( + + + + + + } /> + } /> + }/> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ); +} + +export default App; \ No newline at end of file diff --git a/client/src/UserContext.js b/client/src/UserContext.js new file mode 100644 index 0000000..3eb9121 --- /dev/null +++ b/client/src/UserContext.js @@ -0,0 +1,11 @@ +import React from 'react'; + +// Creates a Context object +// A context object as the name states is a data type of an object that can be used to store information that can be shared to other components within the app +// The context object is a different approach to passing information between components and allows easier access by avoiding the use of prop-drilling +const UserContext = React.createContext(); + +// The "Provider" component allows other components to consume/use the context object and supply the necessary information needed to the context object +export const UserProvider = UserContext.Provider; + +export default UserContext; \ No newline at end of file diff --git a/client/src/components/AdminView.js b/client/src/components/AdminView.js new file mode 100644 index 0000000..16be126 --- /dev/null +++ b/client/src/components/AdminView.js @@ -0,0 +1,60 @@ +import { useState, useEffect } from 'react'; +import { Table } from 'react-bootstrap'; + +import EditCourse from './EditCourse'; +import ArchiveCourse from './ArchiveCourse'; + + +export default function AdminView({ coursesData, fetchData }) { + + + const [courses, setCourses] = useState([]) + + + //Getting the coursesData from the courses page + useEffect(() => { + const coursesArr = coursesData.map(course => { + return ( + + {course._id} + {course.name} + {course.description} + {course.price} + + {course.isActive ? "Available" : "Unavailable"} + + + + + ) + }) + + setCourses(coursesArr) + + }, [coursesData]) + + + return( + <> +

Admin Dashboard

+ + + + + + + + + + + + + + + {courses} + +
IDNameDescriptionPriceAvailabilityActions
+ + + ) +} \ No newline at end of file diff --git a/client/src/components/AppNavbar.js b/client/src/components/AppNavbar.js new file mode 100644 index 0000000..2e9cbcf --- /dev/null +++ b/client/src/components/AppNavbar.js @@ -0,0 +1,51 @@ +import { useState, useContext } from 'react'; +import Container from 'react-bootstrap/Container'; +import Navbar from 'react-bootstrap/Navbar'; +import Nav from 'react-bootstrap/Nav'; + +import { Link, NavLink } from 'react-router-dom'; +import UserContext from '../UserContext'; + + +export default function AppNavbar() { + + // State to store the user information stored in the login page. + // const [user, setUser] = useState(localStorage.getItem("access")); + // console.log(user); + + const { user } = useContext(UserContext); + + return( + + + Zuitt + + + + + + + ) +} \ No newline at end of file diff --git a/client/src/components/ArchiveCourse.js b/client/src/components/ArchiveCourse.js new file mode 100644 index 0000000..8e3c313 --- /dev/null +++ b/client/src/components/ArchiveCourse.js @@ -0,0 +1,87 @@ +import { Button } from 'react-bootstrap'; +import Swal from 'sweetalert2'; + +export default function ArchiveCourse({course, isActive, fetchData}) { + + const archiveToggle = (courseId) => { + fetch(`${process.env.REACT_APP_API_URL}/courses/${courseId}/archive`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('token')}` + } + }) + + .then(res => res.json()) + .then(data => { + console.log(data) + if(data === true) { + Swal.fire({ + title: 'Success', + icon: 'success', + text: 'Course successfully disabled' + }) + fetchData(); + + }else { + Swal.fire({ + title: 'Something Went Wrong', + icon: 'Error', + text: 'Please Try again' + }) + fetchData(); + } + + + }) + } + + + const activateToggle = (courseId) => { + fetch(`${process.env.REACT_APP_API_URL}/courses/${courseId}/activate`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('token')}` + } + }) + + .then(res => res.json()) + .then(data => { + console.log(data) + if(data === true) { + Swal.fire({ + title: 'Success', + icon: 'success', + text: 'Course successfully enabled' + }) + fetchData(); + }else { + Swal.fire({ + title: 'Something Went Wrong', + icon: 'Error', + text: 'Please Try again' + }) + fetchData(); + } + + + }) + } + + + return( + <> + {isActive ? + + + + : + + + + } + + + ) +} \ No newline at end of file diff --git a/client/src/components/Banner.js b/client/src/components/Banner.js new file mode 100644 index 0000000..9896653 --- /dev/null +++ b/client/src/components/Banner.js @@ -0,0 +1,18 @@ +import { Button, Row, Col } from 'react-bootstrap'; +import { Link } from 'react-router-dom'; + +export default function Banner({data}) { + + console.log(data); + const {title, content, destination, label} = data; + + return ( + + +

{title}

+

{content}

+ {label} + +
+ ) +} \ No newline at end of file diff --git a/client/src/components/CourseCard.js b/client/src/components/CourseCard.js new file mode 100644 index 0000000..2c676d8 --- /dev/null +++ b/client/src/components/CourseCard.js @@ -0,0 +1,56 @@ +import { useState } from 'react'; +import { Card, Button } from 'react-bootstrap'; +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; + +export default function CourseCard({courseProp}) { + + // Checks to see if the data was successfully passed + // console.log(props); + // Every component recieves information in a form of an object + // console.log(typeof props); + + // Deconstruct the course properties into their own variables + const { _id, name, description, price} = courseProp; + + + // const [count, setCount] = useState(0); + // console.log(useState(0)); + + // const [seats, setSeats] = useState(10); + + // function enroll(){ + // if (seats > 0) { + // setCount(count + 1); + // console.log('Enrollees: ' + count); + // setSeats(seats - 1); + // console.log('Seats: ' + seats) + // } else { + // alert("No more seats available"); + // }; + // } + + return ( + + + {name} + Description: + {description} + Price: + PhP {price} + Details + + + ) +} + + +CourseCard.propTypes = { + // The "shape" method is used to check if a prop object conforms to a specific "shape" + course: PropTypes.shape({ + // Define the properties and their expected types + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + price: PropTypes.number.isRequired + }) +} \ No newline at end of file diff --git a/client/src/components/CourseSearch.js b/client/src/components/CourseSearch.js new file mode 100644 index 0000000..97966a2 --- /dev/null +++ b/client/src/components/CourseSearch.js @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; +import CourseCard from './CourseCard'; +const CourseSearch = () => { + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + + const handleSearch = async () => { + try { + const response = await fetch(`${process.env.REACT_APP_API_URL}/courses/searchByName`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ courseName: searchQuery }) + }); + const data = await response.json(); + setSearchResults(data); + } catch (error) { + console.error('Error searching for courses:', error); + } + }; + + return ( +
+

Course Search

+
+ + setSearchQuery(event.target.value)} + /> +
+ +

Search Results:

+
    + {searchResults.map(course => ( + //
  • {course.name}
  • + + ))} +
+
+ ); +}; + +export default CourseSearch; \ No newline at end of file diff --git a/client/src/components/EditCourse.js b/client/src/components/EditCourse.js new file mode 100644 index 0000000..566e9ec --- /dev/null +++ b/client/src/components/EditCourse.js @@ -0,0 +1,135 @@ +import React, { useState } from 'react'; +import { Button, Modal, Form } from 'react-bootstrap'; +import Swal from 'sweetalert2'; + +export default function EditCourse({ course, fetchData }) { + + //state for courseId for the fetch URL + const [courseId, setCourseId] = useState(''); + + //Forms state + //Add state for the forms of course + const [name, setName] = useState(''); + const [description, setDescription] = useState('') + const [price, setPrice] = useState('') + + //state for editCourse Modals to open/close + const [showEdit, setShowEdit] = useState(false) + + //function for opening the modal + const openEdit = (courseId) => { + //to still get the actual data from the form + fetch(`${process.env.REACT_APP_API_URL}/courses/${ courseId }`) + .then(res => res.json()) + .then(data => { + //populate all the input values with course info that we fetched + setCourseId(data._id); + setName(data.name); + setDescription(data.description); + setPrice(data.price) + }) + + //Then, open the modal + setShowEdit(true) + } + + const closeEdit = () => { + setShowEdit(false); + setName('') + setDescription('') + setPrice(0) + } + + + //function to update the course + const editCourse = (e, courseId) => { + e.preventDefault(); + + fetch(`${process.env.REACT_APP_API_URL}/courses/${ courseId }`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify({ + name: name, + description: description, + price: price + }) + }) + .then(res => res.json()) + .then(data => { + console.log(data) + + if(data === true) { + Swal.fire({ + title: 'Success!', + icon: 'success', + text: 'Course Successfully Updated' + }) + closeEdit(); + fetchData(); + + } else { + Swal.fire({ + title: 'Error!', + icon: 'error', + text: 'Please try again' + }) + closeEdit(); + fetchData(); + } + }) + } + + + return( + <> + + + {/*Edit Modal Forms*/} + +
editCourse(e, courseId)}> + + Edit Course + + + + + Name + setName(e.target.value)} + required/> + + + + Description + setDescription(e.target.value)} + required/> + + + + Price + setPrice(e.target.value)} + required/> + + + + + + + +
+ +
+ + ) +} \ No newline at end of file diff --git a/client/src/components/FeaturedCourses.js b/client/src/components/FeaturedCourses.js new file mode 100644 index 0000000..140f06c --- /dev/null +++ b/client/src/components/FeaturedCourses.js @@ -0,0 +1,51 @@ +import { useState, useEffect } from 'react' +import { CardGroup } from 'react-bootstrap' +import PreviewCourses from './PreviewCourses' + +export default function FeaturedCourses(){ + + const [previews, setPreviews] = useState([]) + + useEffect(() => { + fetch(`${ process.env.REACT_APP_API_URL}/courses/`) + .then(res => res.json()) + .then(data => { + console.log(data) + + const numbers = [] + const featured = [] + + const generateRandomNums = () => { + let randomNum = Math.floor(Math.random() * data.length) + + if(numbers.indexOf(randomNum) === -1){ + numbers.push(randomNum) + }else{ + generateRandomNums() + } + } + + for(let i = 0; i < 5; i++){ + generateRandomNums() + + featured.push( + + ) + } + + setPreviews(featured) + }) + }, []) + + return( + <> +

Featured Courses

+ + + + {previews} + + + + ) +} \ No newline at end of file diff --git a/client/src/components/Highlights.js b/client/src/components/Highlights.js new file mode 100644 index 0000000..6cb2b64 --- /dev/null +++ b/client/src/components/Highlights.js @@ -0,0 +1,44 @@ +import { Row, Col, Card } from 'react-bootstrap'; + +export default function Highlights() { + return ( + + + + + +

Learn from Home

+
+ + Pariatur adipisicing aute do amet dolore cupidatat. Eu labore aliqua eiusmod commodo occaecat mollit ullamco labore minim. Minim irure fugiat anim ea sint consequat fugiat laboris id. Lorem elit irure mollit officia incididunt ea ullamco laboris excepteur amet. Cillum pariatur consequat adipisicing aute ex. + +
+
+ + + + + +

Study Now, Pay Later

+
+ + Ex Lorem cillum consequat ad. Consectetur enim sunt amet sit nulla dolor exercitation est pariatur aliquip minim. Commodo velit est in id anim deserunt ullamco sint aute amet. Adipisicing est Lorem aliquip anim occaecat consequat in magna nisi occaecat consequat et. Reprehenderit elit dolore sunt labore qui. + +
+
+ + + + + +

Be Part of Our Community

+
+ + Minim nostrud dolore consequat ullamco minim aliqua tempor velit amet. Officia occaecat non cillum sit incididunt id pariatur. Mollit tempor laboris commodo anim mollit magna ea reprehenderit fugiat et reprehenderit tempor. Qui ea Lorem dolor in ad nisi anim. Culpa adipisicing enim et officia exercitation adipisicing. + +
+
+ +
+ ) +} \ No newline at end of file diff --git a/client/src/components/PreviewCourses.js b/client/src/components/PreviewCourses.js new file mode 100644 index 0000000..db2e864 --- /dev/null +++ b/client/src/components/PreviewCourses.js @@ -0,0 +1,26 @@ +import { Col, Card } from 'react-bootstrap' +import { Link } from 'react-router-dom' + +export default function Product(props){ + const { breakPoint, data } = props + + const { _id, name, description, price } = data + + return( + + + + + {name} + + {description} + + + +
₱{price}
+ Details +
+
+ + ) +} diff --git a/client/src/components/ResetPassword.js b/client/src/components/ResetPassword.js new file mode 100644 index 0000000..9ca1160 --- /dev/null +++ b/client/src/components/ResetPassword.js @@ -0,0 +1,80 @@ +import React, { useState } from 'react'; + +const ResetPassword = () => { + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [message, setMessage] = useState(''); + + const handleResetPassword = async (e) => { + e.preventDefault(); + + if (password !== confirmPassword) { + setMessage('Passwords do not match'); + return; + } + + try { + const token = localStorage.getItem('token'); // Replace with your actual JWT token + const response = await fetch(`${process.env.REACT_APP_API_URL}/users/reset-password`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ newPassword: password }), + }); + + if (response.ok) { + setMessage('Password reset successfully'); + setPassword(''); + setConfirmPassword(''); + } else { + const errorData = await response.json(); + setMessage(errorData.message); + } + } catch (error) { + setMessage('An error occurred. Please try again.'); + console.error(error); + } + }; + + return ( +
+

Reset Password

+
+
+ + setPassword(e.target.value)} + required + /> +
+
+ + setConfirmPassword(e.target.value)} + required + /> +
+ {message &&
{message}
} + +
+
+ ); +}; + +export default ResetPassword; diff --git a/client/src/components/SearchByPrice.js b/client/src/components/SearchByPrice.js new file mode 100644 index 0000000..40e6602 --- /dev/null +++ b/client/src/components/SearchByPrice.js @@ -0,0 +1,77 @@ +import React, { useState } from 'react'; + +const SearchByPrice = () => { + const [minPrice, setMinPrice] = useState(''); + const [maxPrice, setMaxPrice] = useState(''); + const [courses, setCourses] = useState([]); + + const handleMinPriceChange = (e) => { + setMinPrice(e.target.value); + }; + + const handleMaxPriceChange = (e) => { + setMaxPrice(e.target.value); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + const requestOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ minPrice, maxPrice }), + }; + + fetch(`${process.env.REACT_APP_API_URL}/courses/searchByPrice`, requestOptions) + .then((response) => response.json()) + .then((data) => { + setCourses(data.courses); + }) + .catch((error) => { + console.error('Error:', error); + }); + }; + + return ( +
+

Search Courses by Price Range

+
+
+ + +
+
+ + +
+ +
+

Search Results:

+
    + {courses.map((course) => ( +
  • {course.name}
  • + ))} +
+
+ ); +}; + +export default SearchByPrice; \ No newline at end of file diff --git a/client/src/components/UpdateProfile.js b/client/src/components/UpdateProfile.js new file mode 100644 index 0000000..54d93e5 --- /dev/null +++ b/client/src/components/UpdateProfile.js @@ -0,0 +1,90 @@ +import React, { useState } from 'react'; + +const UpdateProfileForm = () => { + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [mobileNo, setMobileNo] = useState(''); + + const handleSubmit = (e) => { + e.preventDefault(); + + const token = localStorage.getItem('token'); + + const profileData = { + firstName, + lastName, + mobileNo, + }; + + fetch('http://localhost:4000/users/profile', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(profileData), + }) + .then(res=>res.json()) + .then((response) => { + if (response._id) { + // Profile update was successful, refresh the page + window.location.reload(); + } else { + throw new Error('Profile update failed'); + } + }) + .catch((error) => { + console.error(error); + // Handle error here + }); + }; + + return ( +
+

Update Profile

+
+
+ + setFirstName(e.target.value)} + /> +
+
+ + setLastName(e.target.value)} + /> +
+
+ + setMobileNo(e.target.value)} + /> +
+ +
+
+ ); +}; + +export default UpdateProfileForm; diff --git a/client/src/components/UserView.js b/client/src/components/UserView.js new file mode 100644 index 0000000..998d966 --- /dev/null +++ b/client/src/components/UserView.js @@ -0,0 +1,33 @@ +import React, { useState, useEffect } from 'react'; +import CourseCard from './CourseCard'; +import CourseSearch from './CourseSearch'; + + +export default function UserView({coursesData}) { + + const [courses, setCourses] = useState([]) + + useEffect(() => { + const coursesArr = coursesData.map(course => { + //only render the active courses + if(course.isActive === true) { + return ( + + ) + } else { + return null; + } + }) + + //set the courses state to the result of our map function, to bring our returned course component outside of the scope of our useEffect where our return statement below can see. + setCourses(coursesArr) + + }, [coursesData]) + + return( + <> + + { courses } + + ) +} \ No newline at end of file diff --git a/client/src/data/coursesData.js b/client/src/data/coursesData.js new file mode 100644 index 0000000..83596cf --- /dev/null +++ b/client/src/data/coursesData.js @@ -0,0 +1,25 @@ +const coursesData = [ + { + id: "wdc001", + name: "PHP - Laravel", + description: "Nostrud velit dolor excepteur ullamco consectetur aliquip tempor. Consectetur occaecat laborum exercitation sint reprehenderit irure nulla mollit. Do dolore sint deserunt quis ut sunt ad nulla est consectetur culpa. Est esse dolore nisi consequat nostrud id nostrud sint sint deserunt dolore.", + price: 45000, + onOffer: true + }, + { + id: "wdc002", + name: "Python - Django", + description: "Eu non commodo et eu ex incididunt minim aliquip anim. Aliquip voluptate ut velit fugiat laborum. Laborum dolore anim pariatur pariatur commodo minim ut officia mollit ad ipsum ex. Laborum veniam cupidatat veniam minim occaecat veniam deserunt nulla irure. Enim elit sint magna incididunt occaecat in dolor amet dolore consectetur ad mollit. Exercitation sunt occaecat labore irure proident consectetur commodo ad anim ea tempor irure.", + price: 50000, + onOffer: true + }, + { + id: "wdc003", + name: "Java - Springboot", + description: "Proident est adipisicing est deserunt cillum dolore. Fugiat incididunt quis aliquip ut aliquip est mollit officia dolor ea cupidatat velit. Consectetur aute velit aute ipsum quis. Eiusmod dolor exercitation dolor mollit duis velit aliquip dolor proident ex exercitation labore cupidatat. Eu aliquip mollit labore do.", + price: 55000, + onOffer: true + } +] + +export default coursesData; \ No newline at end of file diff --git a/client/src/index.js b/client/src/index.js new file mode 100644 index 0000000..f58cf40 --- /dev/null +++ b/client/src/index.js @@ -0,0 +1,29 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import 'bootstrap/dist/css/bootstrap.min.css'; + + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); + + +// const name = 'John Smith'; +// const user = { +// firstName: 'Jane', +// lastName: 'Smith' +// } + +// function formatName(user) { +// return user.firstName + ' ' + user.lastName; +// } + +// const element =

Hello, {formatName(user)}

+ +// const root = ReactDOM.createRoot(document.getElementById('root')); + +// root.render(element) \ No newline at end of file diff --git a/client/src/pages/AddCourse.js b/client/src/pages/AddCourse.js new file mode 100644 index 0000000..e12a7f2 --- /dev/null +++ b/client/src/pages/AddCourse.js @@ -0,0 +1,101 @@ +import {useState,useEffect, useContext} from 'react'; +import {Form,Button} from 'react-bootstrap'; +import { Navigate, useNavigate } from 'react-router-dom'; +import Swal from 'sweetalert2'; +import UserContext from '../UserContext'; + +export default function AddCourse(){ + + const navigate = useNavigate(); + + const {user} = useContext(UserContext); + + //input states + const [name,setName] = useState(""); + const [description,setDescription] = useState(""); + const [price,setPrice] = useState(""); + + function createCourse(e){ + + //prevent submit event's default behavior + e.preventDefault(); + + let token = localStorage.getItem('token'); + console.log(token); + + fetch(`${process.env.REACT_APP_API_URL}/courses/`,{ + + method: 'POST', + headers: { + "Content-Type": "application/json", + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + + name: name, + description: description, + price: price + + }) + }) + .then(res => res.json()) + .then(data => { + + //data is the response of the api/server after it's been process as JS object through our res.json() method. + console.log(data); + + if(data){ + Swal.fire({ + + icon:"success", + title: "Course Added" + + }) + + navigate("/courses"); + } else { + Swal.fire({ + + icon: "error", + title: "Unsuccessful Course Creation", + text: data.message + + }) + } + + }) + + setName("") + setDescription("") + setPrice(0); + } + + return ( + + (user.isAdmin === true) + ? + <> +

Add Course

+
createCourse(e)}> + + Name: + {setName(e.target.value)}}/> + + + Description: + {setDescription(e.target.value)}}/> + + + Price: + {setPrice(e.target.value)}}/> + + +
+ + : + + + ) + + +} \ No newline at end of file diff --git a/client/src/pages/CourseView.js b/client/src/pages/CourseView.js new file mode 100644 index 0000000..72b86e5 --- /dev/null +++ b/client/src/pages/CourseView.js @@ -0,0 +1,105 @@ +import { useState, useEffect, useContext } from 'react'; +import { Container, Card, Button, Row, Col } from 'react-bootstrap'; +import { useParams, useNavigate, Link } from 'react-router-dom'; +import Swal from 'sweetalert2'; +import UserContext from '../UserContext'; + +export default function CourseView() { + + // The "useParams" hook allows us to retrieve any parameter or the courseId passed via the URL + const { courseId } = useParams(); + const { user } = useContext(UserContext); + // Allows us to gain access to methods that will allow us to redirect a user to a different page after enrolling a course + + //an object with methods to redirect the user + const navigate = useNavigate(); + + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [price, setPrice] = useState(0); + + const enroll = (courseId) => { + + fetch(`${process.env.REACT_APP_API_URL}/users/enroll`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${ localStorage.getItem('access') }` + }, + body: JSON.stringify({ + courseId: courseId + }) + }) + .then(res => res.json()) + .then(data => { + + console.log(data.message); + + if (data.message === 'Enrolled Successfully.') { + + Swal.fire({ + title: "Successfully enrolled", + icon: 'success', + text: "You have successfully enrolled for this course." + }); + + // The "navigate" method allows us to redirect the user to a different page and is an easier approach rather than using the "Navigate" component + navigate("/courses"); + + } else { + + Swal.fire({ + title: "Something went wrong", + icon: "error", + text: "Please try again." + }); + + } + + }); + + }; + + useEffect(()=> { + + console.log(courseId); + + fetch(`${process.env.REACT_APP_API_URL}/courses/${courseId}`) + .then(res => res.json()) + .then(data => { + + console.log(data); + + setName(data.name); + setDescription(data.description); + setPrice(data.price); + + }); + + }, [courseId]); + + return( + + + + + + {name} + Description: + {description} + Price: + PhP {price} + Class Schedule + 8 am - 5 pm + { user.id !== null ? + + : + Log in to Enroll + } + + + + + + ) +} \ No newline at end of file diff --git a/client/src/pages/Courses.js b/client/src/pages/Courses.js new file mode 100644 index 0000000..96b4daf --- /dev/null +++ b/client/src/pages/Courses.js @@ -0,0 +1,74 @@ +import { useEffect, useState, useContext } from 'react'; +import CourseCard from '../components/CourseCard'; +// import coursesData from '../data/coursesData'; +import UserContext from '../UserContext'; +import UserView from '../components/UserView'; +import AdminView from '../components/AdminView'; + +export default function Courses() { + + const { user } = useContext(UserContext); + + // Checks to see if the mock data was captured + // console.log(coursesData); + // console.log(coursesData[0]); + + // State that will be used to store the courses retrieved from the database + const [courses, setCourses] = useState([]); + + + const fetchData = () => { + fetch(`${process.env.REACT_APP_API_URL}/courses/all`) + .then(res => res.json()) + .then(data => { + + console.log(data); + + // Sets the "courses" state to map the data retrieved from the fetch request into several "CourseCard" components + setCourses(data); + + }); + } + + + // Retrieves the courses from the database upon initial render of the "Courses" component + useEffect(() => { + + fetchData() + + }, []); + + // The "map" method loops through the individual course objects in our array and returns a component for each course + // Multiple components created through the map method must have a unique key that will help React JS identify which components/elements have been changed, added or removed + // Everytime the map method loops through the data, it creates a "CourseCard" component and then passes the current element in our coursesData array using the courseProp + // const courses = coursesData.map(course => { + // return ( + // + // ); + // }) + + return( + <> + { + (user.isAdmin === true) ? + + + : + + + + } + + ) +} + + + + + + + + + + + diff --git a/client/src/pages/Error.js b/client/src/pages/Error.js new file mode 100644 index 0000000..bc00dcb --- /dev/null +++ b/client/src/pages/Error.js @@ -0,0 +1,15 @@ +import Banner from '../components/Banner'; + +export default function Error() { + + const data = { + title: "404 - Not found", + content: "The page you are looking for cannot be found", + destination: "/", + label: "Back home" + } + + return ( + + ) +} \ No newline at end of file diff --git a/client/src/pages/Home.js b/client/src/pages/Home.js new file mode 100644 index 0000000..cdc0395 --- /dev/null +++ b/client/src/pages/Home.js @@ -0,0 +1,23 @@ +import Banner from '../components/Banner'; +import Highlights from '../components/Highlights'; +import FeaturedCourses from '../components/FeaturedCourses'; +// import CourseCard from '../components/CourseCard'; + +export default function Home() { + + const data = { + title: "Zuitt Coding Bootcamp", + content: "Opportunities for everyone, everywhere", + destination: "/courses", + label: "Enroll now!" + } + + return ( + <> + + + + + + ) +} \ No newline at end of file diff --git a/client/src/pages/Login.js b/client/src/pages/Login.js new file mode 100644 index 0000000..91237cb --- /dev/null +++ b/client/src/pages/Login.js @@ -0,0 +1,152 @@ +import { useState, useEffect, useContext } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { Navigate } from 'react-router-dom'; +import Swal from 'sweetalert2'; +import UserContext from '../UserContext'; + +export default function Login() { + + const { user, setUser } = useContext(UserContext); + + // State hooks to store the values of the input fields + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + // State to determine whether submit button is enabled or not + const [isActive, setIsActive] = useState(true); + + + function authenticate(e) { + + // Prevents page redirection via form submission + e.preventDefault(); + fetch(`${process.env.REACT_APP_API_URL}/users/login`,{ + + method: 'POST', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + + email: email, + password: password + + }) + }) + .then(res => res.json()) + .then(data => { + + if(data.access){ + + // Set the email of the authenticated user in the local storage + // Syntax + // localStorage.setItem('propertyName', value); + localStorage.setItem('token', data.access); + retrieveUserDetails(data.access); + + setUser({ + access: localStorage.getItem('token') + }) + + Swal.fire({ + title: "Login Successful", + icon: "success", + text: "Welcome to Zuitt!" + }); + + } else { + + Swal.fire({ + title: "Authentication failed", + icon: "error", + text: "Check your login details and try again." + }); + } + }) + // Clear input fields after submission + setEmail(''); + setPassword(''); + + + } + + + const retrieveUserDetails = (token) => { + + // The token will be sent as part of the request's header information + // We put "Bearer" in front of the token to follow implementation standards for JWTs + fetch(`${process.env.REACT_APP_API_URL}/users/details`, { + headers: { + Authorization: `Bearer ${ token }` + } + }) + .then(res => res.json()) + .then(data => { + + console.log(data); + + // localStorage.setItem('token', data.access); + + // Changes the global "user" state to store the "id" and the "isAdmin" property of the user which will be used for validation across the whole application + setUser({ + id: data._id, + isAdmin: data.isAdmin + }); + + }) + + }; + + useEffect(() => { + + // Validation to enable submit button when all fields are populated and both passwords match + if(email !== '' && password !== ''){ + setIsActive(true); + }else{ + setIsActive(false); + } + + }, [email, password]); + + return ( + (user.id !== null) ? + + + + : + +
authenticate(e)}> +

Login

+ + Email address + setEmail(e.target.value)} + required + /> + + + + Password + setPassword(e.target.value)} + required + /> + + + { isActive ? + + : + + } +
+ ) +} \ No newline at end of file diff --git a/client/src/pages/Logout.js b/client/src/pages/Logout.js new file mode 100644 index 0000000..2ff164d --- /dev/null +++ b/client/src/pages/Logout.js @@ -0,0 +1,29 @@ +import { useContext, useEffect } from 'react'; +import { Navigate } from 'react-router-dom'; +import UserContext from '../UserContext'; + +export default function Logout() { + + // Consume the UserContext object and destructure it to access the user state and unsetUser function from the context provider + const { unsetUser, setUser } = useContext(UserContext); + + // Clear the localStorage of the user's information + unsetUser(); + + // Placing the "setUser" setter function inside of a useEffect is necessary because of updates within React JS that a state of another component cannot be updated while trying to render a different component + // By adding the useEffect, this will allow the Logout page to render first before triggering the useEffect which changes the state of our user + useEffect(() => { + // Set the user state back to it's original value + setUser({ + id: null, + isAdmin: null + }); + + }, []) + + // Navigate back to login + return ( + + ) + +} \ No newline at end of file diff --git a/client/src/pages/Profile.js b/client/src/pages/Profile.js new file mode 100644 index 0000000..4f96b9a --- /dev/null +++ b/client/src/pages/Profile.js @@ -0,0 +1,71 @@ +import {useState,useEffect, useContext} from 'react'; +import {Row, Col} from 'react-bootstrap'; +import UserContext from '../UserContext'; +import { useNavigate,Navigate } from 'react-router-dom'; +import ResetPassword from '../components/ResetPassword'; +import UpdateProfileForm from '../components/UpdateProfile'; +export default function Profile(){ + + const {user} = useContext(UserContext); + + const [details,setDetails] = useState({}) + + useEffect(()=>{ + + fetch(`${process.env.REACT_APP_API_URL}/users/details`, { + headers: { + Authorization: `Bearer ${ localStorage.getItem('token') }` + } + }) + .then(res => res.json()) + .then(data => { + console.log(data) + // Set the user states values with the user details upon successful login. + if (typeof data._id !== "undefined") { + + setDetails(data); + + } + }); + + },[]) + + return ( + // (user.email === null) ? + // + // : + (user.id === null) ? + + : + <> + + +

Profile

+ {/*

James Dela Cruz

*/} +

{`${details.firstName} ${details.lastName}`}

+
+

Contacts

+
    + {/*
  • Email: {user.email}
  • */} +
  • Email: {details.email}
  • + {/*
  • Mobile No: 09266772411
  • */} +
  • Mobile No: {details.mobileNo}
  • +
+ +
+ + + + + + + + + + + + + + ) + +} \ No newline at end of file diff --git a/client/src/pages/Register.js b/client/src/pages/Register.js new file mode 100644 index 0000000..3ac43de --- /dev/null +++ b/client/src/pages/Register.js @@ -0,0 +1,164 @@ +import { useState, useEffect, useContext } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { Navigate } from 'react-router-dom'; +import UserContext from '../UserContext'; + +export default function Register() { + + const {user} = useContext(UserContext); + + // State hooks to store the values of the input fields + const [firstName,setFirstName] = useState(""); + const [lastName,setLastName] = useState(""); + const [email,setEmail] = useState(""); + const [mobileNo,setMobileNo] = useState(""); + const [password,setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + // State to determine whether submit button is enabled or not + const [isActive, setIsActive] = useState(false); + + // Check if values are successfully binded + console.log(firstName); + console.log(lastName); + console.log(email); + console.log(mobileNo); + console.log(password); + console.log(confirmPassword) + + + function registerUser(e) { + + // Prevents page redirection via form submission + e.preventDefault(); + + fetch(`${process.env.REACT_APP_API_URL}/users/register`,{ + + method: 'POST', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + + firstName: firstName, + lastName: lastName, + email: email, + mobileNo: mobileNo, + password: password + + }) + }) + .then(res => res.json()) + .then(data => { + + //data is the response of the api/server after it's been process as JS object through our res.json() method. + console.log(data); + //data will only contain an email property if we can properly save our user. + if(data){ + + setFirstName(''); + setLastName(''); + setEmail(''); + setMobileNo(''); + setPassword(''); + setConfirmPassword(''); + + + alert("Thank you for registering!") + + } else { + + alert("Please try again later.") + } + + }) + } + + + + useEffect(()=>{ + + if((firstName !== "" && lastName !== "" && email !== "" && mobileNo !== "" && password !=="" && confirmPassword !=="") && (password === confirmPassword) && (mobileNo.length === 11)){ + + setIsActive(true) + + } else { + + setIsActive(false) + + } + + },[firstName,lastName,email,mobileNo,password,confirmPassword]) + + return ( + + (user.id !== null) ? + + : + +
registerUser(e)}> +

Register

+ + First Name: + {setFirstName(e.target.value)}} + /> + + + Last Name: + {setLastName(e.target.value)}}/> + + + Email: + {setEmail(e.target.value)}}/> + + + Mobile No: + {setMobileNo(e.target.value)}}/> + + + Password: + {setPassword(e.target.value)}}/> + + + Confirm Password: + {setConfirmPassword(e.target.value)}}/> + + { + isActive + + ? + : + } +
+ + ) +} \ No newline at end of file diff --git a/server/.env b/server/.env new file mode 100644 index 0000000..df87c26 --- /dev/null +++ b/server/.env @@ -0,0 +1,2 @@ +PORT=4000 +CONNECTION_STRING=/bookingAPI?retryWrites=true&w=majority \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..30bc162 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/server/auth.js b/server/auth.js new file mode 100644 index 0000000..3d33bc1 --- /dev/null +++ b/server/auth.js @@ -0,0 +1,98 @@ +// [Section] JSON Web Tokens + const jwt = require("jsonwebtoken"); + +// [Section] Secret Keyword + const secret = "CourseBookingAPI"; + + + + +// [Section] Token creation + module.exports.createAccessToken = (user) => { + const data = { + id : user._id, + email : user.email, + isAdmin : user.isAdmin + }; + return jwt.sign(data, secret, {}); + }; + + + +//[Section] Token Verification + /* + - Analogy + Receive the gift and open the lock to verify if the the sender is legitimate and the gift was not tampered with + */ + + module.exports.verify = (req, res, next) => { + console.log(req.headers.authorization); + + //req.headers.authorization contains sensitive data and especially our token + let token = req.headers.authorization; + + //This if statement will first check IF token variable contains undefined or a proper jwt. If it is undefined, we will check token's data type with typeof, then send a message to the client. + if(typeof token === "undefined"){ + return res.send({auth: "Failed. No Token"}); + } else { + console.log(token); + token = token.slice(7, token.length); + console.log(token); + +//[SECTION] Token decryption + /* + - Analogy + Open the gift and get the content + */ + + // Validate the token using the "verify" method decrypting the token using the secret code + jwt.verify(token, secret, function(err, decodedToken){ + if(err){ + return res.send({ + auth: "Failed", + message: err.message + }); + } else { + console.log(decodedToken);//contains the data from our token + req.user = decodedToken + next(); + } + }) + } + }; + + + + +//[Section] verifyAdmin will also be used a middleware. +module.exports.verifyAdmin = (req, res, next) => { + if(req.user.isAdmin){ + next(); + } else { + return res.send({ + auth: "Failed", + message: "Action Forbidden" + }) + } +} + + + + + + + + + + + + + + + + + + + + + diff --git a/server/controllers/course.js b/server/controllers/course.js new file mode 100644 index 0000000..d0a0cb9 --- /dev/null +++ b/server/controllers/course.js @@ -0,0 +1,245 @@ +//[SECTION] Dependencies and Modules + const Course = require("../models/Course"); + const User = require("../models/User"); + + +//[SECTION] Create a new course + + module.exports.addCourse = (req, res) => { + let newCourse = new Course({ + name : req.body.name, + description : req.body.description, + price : req.body.price + }); + + return newCourse.save().then((course, error) => { + // Course creation successful + if (error) { + return res.send(false); + + // Course creation failed + } else { + return res.send(true); + } + }) + .catch(err => res.send(err)) + } + + +//[SECTION] Retrieve all courses + module.exports.getAllCourses = (req, res) => { + return Course.find({}).then(result => { + return res.send(result); + }) + .catch(err => res.send(err)) + }; + + +//[SECTION] Retrieve all ACTIVE courses + module.exports.getAllActive = (req, res) => { + return Course.find({ isActive : true }).then(result => { + return res.send(result); + }) + .catch(err => res.send(err)) + }; + +//[SECTION] Retrieving a specific course + module.exports.getCourse = (req, res) => { + return Course.findById(req.params.courseId).then(result => { + return res.send(result); + }) + .catch(err => res.send(err)) + }; + + +//[SECTION] Update a course + module.exports.updateCourse = (req, res) => { + // Specify the fields/properties of the document to be updated + let updatedCourse = { + name : req.body.name, + description : req.body.description, + price : req.body.price + }; + + // Syntax + // findByIdAndUpdate(document ID, updatesToBeApplied) + return Course.findByIdAndUpdate(req.params.courseId, updatedCourse).then((course, error) => { + + // Course not updated + if (error) { + return res.send(false); + + // Course updated successfully + } else { + return res.send(true); + } + }) + .catch(err => res.send(err)) + }; + + +//SECTION] Archive a course + module.exports.archiveCourse = (req, res) => { + + let updateActiveField = { + isActive: false + } + + return Course.findByIdAndUpdate(req.params.courseId, updateActiveField) + .then((course, error) => { + + //course archived successfully + if(error){ + return res.send(false) + + // failed + } else { + return res.send(true) + } + }) + .catch(err => res.send(err)) + + }; + + +//[SECTION] Activate a course + module.exports.activateCourse = (req, res) => { + + let updateActiveField = { + isActive: true + } + + return Course.findByIdAndUpdate(req.params.courseId, updateActiveField) + .then((course, error) => { + + //course archived successfully + if(error){ + return res.send(false) + + // failed + } else { + return res.send(true) + } + }) + .catch(err => res.send(err)) + + }; + +//ChatGPT Generated Code + +module.exports.searchCoursesByName = async (req, res) => { + try { + const { courseName } = req.body; + + // Use a regular expression to perform a case-insensitive search + const courses = await Course.find({ + name: { $regex: courseName, $options: 'i' } + }); + + res.json(courses); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}; + +// Controller to get the list of emails of users enrolled in a course +//Solution +module.exports.getEmailsOfEnrolledUsers = async (req, res) => { + const courseId = req.params.courseId; + + try { + // Find the course by courseId + const course = await Course.findById(courseId); + + if (!course) { + return res.status(404).json({ message: 'Course not found' }); + } + + // Get the userIds of enrolled users from the course + const userIds = course.enrollees.map(enrollee => enrollee.userId); + + // Find the users with matching userIds + const enrolledUsers = await User.find({ _id: { $in: userIds } }); + + // Extract the emails from the enrolled users + const emails = enrolledUsers.map(user => user.email); + + res.status(200).json({ emails }); + } catch (error) { + res.status(500).json({ message: 'An error occurred while retrieving enrolled users' }); + } +}; + +//Given to students: +/* + const getEmailsOfEnrolledUsers = async (req, res) => { + const courseId = req.body.courseId; + + try { + // Find the course by courseId + const course = await Course.findById(courseId); + + if (!course) { + return res.status(404).json({ message: 'Course not found' }); + } + + // Get the userIds of enrolled users from the course + const userIds = course.enrollees.map(enrollee => enrollee.userId); + + // Find the users with matching userIds + const enrolledUsers = await User.find({ _id: { $in: users } }); + + // Extract the emails from the enrolled users + const emails = enrolledStudents.forEach(user => user.email); + + res.status(200).json({ userEmails }); + } catch (error) { + res.status(500).json({ message: 'An error occurred while retrieving enrolled users' }); + } + }; + + +*/ + +//[ACTIVITY] + + exports.searchCoursesByPriceRange = async (req, res) => { + try { + const { minPrice, maxPrice } = req.body; + + // Find courses within the price range + const courses = await Course.find({ + price: { $gte: minPrice, $lte: maxPrice } + }); + + res.status(200).json({ courses }); + } catch (error) { + res.status(500).json({ error: 'An error occurred while searching for courses' }); + } + }; + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/controllers/user.js b/server/controllers/user.js new file mode 100644 index 0000000..a71d27a --- /dev/null +++ b/server/controllers/user.js @@ -0,0 +1,279 @@ +//[SECTION] Dependencies and Modules + const User = require("../models/User"); + const Course = require("../models/Course"); + const bcrypt = require('bcrypt'); + const auth = require("../auth"); + + +//[SECTION] Check if the email already exists + + module.exports.checkEmailExists = (reqBody) => { + + // The result is sent back to the frontend via the "then" method found in the route file + return User.find({email : reqBody.email}).then(result => { + + // The "find" method returns a record if a match is found + if (result.length > 0) { + return true; + + // No duplicate email found + // The user is not yet registered in the database + } else { + return false; + } + }) + .catch(err => res.send(err)) + }; + + +//[SECTION] User registration + module.exports.registerUser = (reqBody) => { + + // Creates a variable "newUser" and instantiates a new "User" object using the mongoose model + // Uses the information from the request body to provide all the necessary information + let newUser = new User({ + firstName : reqBody.firstName, + lastName : reqBody.lastName, + email : reqBody.email, + mobileNo : reqBody.mobileNo, + password : bcrypt.hashSync(reqBody.password, 10) + }) + + // Saves the created object to our database + return newUser.save().then((user, error) => { + + // User registration failed + if (error) { + return false; + + // User registration successful + } else { + return true; + } + }) + .catch(err => err) + }; + + +//[SECTION] User authentication + + module.exports.loginUser = (req, res) => { + User.findOne({ email : req.body.email} ).then(result => { + + console.log(result); + + // User does not exist + if(result == null){ + + return res.send(false); + + // User exists + } else { + + const isPasswordCorrect = bcrypt.compareSync(req.body.password, result.password); + // If the passwords match/result of the above code is true + if (isPasswordCorrect) { + + return res.send({ access : auth.createAccessToken(result) }) + + // Passwords do not match + } else { + + return res.send(false); + } + } + }) + .catch(err => res.send(err)) + }; + + + + +//[SECTION] Retrieve user details + + module.exports.getProfile = (req, res) => { + + return User.findById(req.user.id) + .then(result => { + result.password = ""; + return res.send(result); + }) + .catch(err => res.send(err)) + }; + + + +//[SECTION] Enroll a registered User + + module.exports.enroll = async (req, res) => { + + console.log(req.user.id) //the user's id from the decoded token after verify() + console.log(req.body.courseId) //the course from our request body + + //process stops here and sends response IF user is an admin + if(req.user.isAdmin){ + return res.send("Action Forbidden") + } + + + let isUserUpdated = await User.findById(req.user.id).then(user => { + let newEnrollment = { + courseId: req.body.courseId, + courseName: req.body.courseName, + courseDescription: req.body.courseDescription, + coursePrice: req.body.coursePrice + } + user.enrollments.push(newEnrollment); + + return user.save().then(user => true).catch(err => err.message) + }) + + + if(isUserUpdated !== true) { + return res.send({message: isUserUpdated}) + } + + let isCourseUpdated = await Course.findById(req.body.courseId).then(course => { + + let enrollee = { + userId: req.user.id + } + + course.enrollees.push(enrollee); + + return course.save().then(course => true).catch(err => err.message) + }) + + + if(isCourseUpdated !== true) { + return res.send({ message: isCourseUpdated}) + } + + + if(isUserUpdated && isCourseUpdated) { + return res.send({ message: "Enrolled Successfully."}) + } + } + + //[ACTIVITY] Getting user's enrolled courses + module.exports.getEnrollments = (req, res) => { + User.findById(req.user.id) + .then(result => res.send(result.enrollments)) + .catch(err => res.send(err)) + } + + + //ChatGPT Generated + + //[SECTION] Reset Password + + module.exports.resetPassword = async (req, res) => { + try { + + //console.log(req.user) + //console.log(req.body) + + const { newPassword } = req.body; + const { id } = req.user; // Extracting user ID from the authorization header + + // Hashing the new password + const hashedPassword = await bcrypt.hash(newPassword, 10); + + // Updating the user's password in the database + await User.findByIdAndUpdate(id, { password: hashedPassword }); + + // Sending a success response + res.status(200).send({ message: 'Password reset successfully' }); + } catch (error) { + console.error(error); + res.status(500).send({ message: 'Internal server error' }); + } + }; + + //[SECTION] Reset Profile + module.exports.updateProfile = async (req, res) => { + try { + + console.log(req.user); + console.log(req.body); + + // Get the user ID from the authenticated token + const userId = req.user.id; + + // Retrieve the updated profile information from the request body + const { firstName, lastName, mobileNo } = req.body; + + // Update the user's profile in the database + const updatedUser = await User.findByIdAndUpdate( + userId, + { firstName, lastName, mobileNo }, + { new: true } + ); + + res.send(updatedUser); + } catch (error) { + console.error(error); + res.status(500).send({ message: 'Failed to update profile' }); + } + } + + //[ACTIVITY] Update Enrollment Status + module.exports.updateEnrollmentStatus = async (req, res) => { + try { + const { userId, courseId, status } = req.body; + + // Find the user and update the enrollment status + const user = await User.findById(userId); + + // Find the enrollment for the course in the user's enrollments array + const enrollment = user.enrollments.find((enrollment) => enrollment.courseId === courseId); + + if (!enrollment) { + return res.status(404).json({ error: 'Enrollment not found' }); + } + + enrollment.status = status; + + // Save the updated user document + await user.save(); + + res.status(200).json({ message: 'Enrollment status updated successfully' }); + } catch (error) { + res.status(500).json({ error: 'An error occurred while updating the enrollment status' }); + } + }; + + +//[ACTIVITY] Update user as admin controller +exports.updateUserAsAdmin = async (req, res) => { + try { + const { userId } = req.body; + + // Find the user and update isAdmin flag + const user = await User.findById(userId); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + user.isAdmin = true; + + // Save the updated user document + await user.save(); + + res.status(200).json({ message: 'User updated as admin successfully' }); + } catch (error) { + res.status(500).json({ error: 'An error occurred while updating the user as admin' }); + } + }; + + + + + + + + + + + diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..1e037a2 --- /dev/null +++ b/server/index.js @@ -0,0 +1,45 @@ +//[SECTION] Dependencies and Modules + const dotenv = require('dotenv').config(); + const express = require("express"); + const mongoose = require("mongoose"); + const cors = require("cors"); + const userRoutes = require("./routes/user"); + const courseRoutes = require("./routes/course"); + + +//[SECTION] Environment Setup + const port = process.env.PORT; + +//[SECTION] Server Setup + const app = express(); + + app.use(cors()) + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + + +//[SECTION] Database Connection + mongoose.connect(process.env.CONNECTION_STRING, { + useNewUrlParser: true, + useUnifiedTopology: true + }); + + mongoose.connection.once('open', () => console.log('Now connected to MongoDB Atlas.')); + + +//[SECTION] Backend Routes + //http://localhost:4000/users + app.use("/users", userRoutes); + //http://localhost:4000/courses + app.use("/courses", courseRoutes); + + +//[SECTION] Server Gateway Response + if(require.main === module) { + app.listen( process.env.PORT || port, () => { + console.log(`API is now online on port ${ process.env.PORT || port }`) + }); + } + + +module.exports = app; diff --git a/server/models/Course.js b/server/models/Course.js new file mode 100644 index 0000000..9c18d84 --- /dev/null +++ b/server/models/Course.js @@ -0,0 +1,41 @@ +//[SECTION] Dependencies and Modules + const mongoose = require('mongoose'); + +//[SECTION] Schema/Blueprint + const courseSchema = new mongoose.Schema({ + name: { + type: String, + required: [true, 'is Required'] + }, + description: { + type: String, + required: [true, 'is Required'] + }, + price: { + type: Number, + required: [true, 'Course Price is Required'] + }, + isActive: { + type: Boolean, + default: true + }, + createdOn: { + type: Date, + default: new Date() + }, + enrollees: [ + { + userId: { + type: String, + required: [true, 'Student ID is Required'] + }, + enrolledOn: { + type: Date, + default: new Date() + } + } + ] + }); + +//[SECTION] Model + module.exports = mongoose.model('Course', courseSchema); \ No newline at end of file diff --git a/server/models/User.js b/server/models/User.js new file mode 100644 index 0000000..8366eff --- /dev/null +++ b/server/models/User.js @@ -0,0 +1,49 @@ +//[SECTION] Modules and Dependencies + const mongoose = require('mongoose'); + +//[SECTION] Schema/Blueprint + const userSchema = new mongoose.Schema({ + firstName: { + type: String, + required: [true, 'First Name is Required'] + }, + lastName: { + type: String, + required: [true, 'Last Name is Required'] + }, + email: { + type: String, + required: [true, 'Email is Required'] + }, + password: { + type: String, + required: [true, 'Password is Required'] + }, + isAdmin: { + type: Boolean, + default: false + }, + mobileNo: { + type: String, + required: [true, 'Mobile Number is Required'] + }, + enrollments: [ + { + courseId: { + type: String, + required: [true, 'Subject ID is Required'] + }, + enrolledOn: { + type: Date, + default: new Date() + }, + status: { + type: String, + default: 'Enrolled' + } + } + ] + }); + +//[SECTION] Model + module.exports = mongoose.model('User', userSchema); \ No newline at end of file diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..8280f0a --- /dev/null +++ b/server/package.json @@ -0,0 +1,23 @@ +{ + "name": "s43-47", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node index", + "dev": "nodemon index" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "bcrypt": "^5.1.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.0", + "mongoose": "^7.2.0", + "nodemon": "^2.0.22" + } +} diff --git a/server/routes/course.js b/server/routes/course.js new file mode 100644 index 0000000..b4fcda1 --- /dev/null +++ b/server/routes/course.js @@ -0,0 +1,56 @@ +//[SECTION] Dependencies and Modules + const express = require('express'); + const courseController = require("../controllers/course"); + const auth = require("../auth") + + const {verify, verifyAdmin} = auth; + +//[SECTION] Routing Component + const router = express.Router(); + + +//[ACTIVITY] create a course POST + router.post("/", verify, verifyAdmin, courseController.addCourse); + + +//[SECTION] Route for retrieving all the courses + router.get("/all", courseController.getAllCourses); + +//[SECTION] Route for retrieving all the ACTIVE courses for all users + // Middleware for verifying JWT is not required because users who aren't logged in should also be able to view the courses + router.get("/", courseController.getAllActive); + + +//[SECTION] Route for Search Course by Name + router.post('/searchByName', courseController.searchCoursesByName); + +//[ACTIVITY] Search Courses By Price Range + router.post('/searchByPrice', courseController.searchCoursesByPriceRange); + +//[SECTION] Route for retrieving a specific course + router.get("/:courseId", courseController.getCourse); + +//[SECTION] Route for updating a course (Admin) + router.put("/:courseId", verify, verifyAdmin, courseController.updateCourse); + +// [SECTION]Route to get the emails of users enrolled in a course + +// Solution + router.get('/:courseId/enrolled-users', courseController.getEmailsOfEnrolledUsers); + +//Given to students: + // router.post('/:courseId/enrolled-users', getEmailsOfEnrolledUsers; + +//[ACTIVITY] Route to archiving a course (Admin) + router.put("/:courseId/archive", verify, verifyAdmin, courseController.archiveCourse); + +//[ACTIVITY] Route to activating a course (Admin) + router.put("/:courseId/activate", verify, verifyAdmin, courseController.activateCourse); + + + + + +// Allows us to export the "router" object that will be accessed in our "index.js" file + module.exports = router; + diff --git a/server/routes/user.js b/server/routes/user.js new file mode 100644 index 0000000..e47cfb3 --- /dev/null +++ b/server/routes/user.js @@ -0,0 +1,71 @@ +//[SECTION] Dependencies and Modules + const express = require('express'); + const userController = require("../controllers/user"); + const auth = require("../auth") + + const {verify, verifyAdmin} = auth; + +//[SECTION] Routing Component + const router = express.Router(); + + + +//[SECTION] Routes - POST + router.post("/checkEmail", (req, res) => { + userController.checkEmailExists(req.body).then(resultFromController => res.send(resultFromController)); + }) + +//[SECTION] Route for user registration + router.post("/register", (req, res) => { + userController.registerUser(req.body).then(resultFromController => res.send(resultFromController)); + }); + +//[SECTION] Route for user authentication + router.post("/login", userController.loginUser); + + +//[ACTIVITY] Route for retrieving user details + //router.post("/details", verify, userController.getProfile); + + //Refactor + router.get("/details", verify, userController.getProfile); + + +//[SECTION] Route to enroll user to a course + router.post('/enroll', verify, userController.enroll); + +//[ACTIVITY] Get Logged User's Enrollments + router.get('/getEnrollments', verify, userController.getEnrollments) + +//ChatGPT Generated Codes + +//[SECTION] Reset Password + router.put('/reset-password', verify, userController.resetPassword); + +//[SECTION] Update Profile + router.put('/profile', verify, userController.updateProfile); + +//[ACTIVITY] Update enrollment status route + router.put('/enrollmentStatusUpdate', userController.updateEnrollmentStatus); + +//[ACTIVITY] Update Admin route + router.put('/updateAdmin', verify, verifyAdmin, userController.updateUserAsAdmin); + +//[SECTION] Export Route System + module.exports = router; + + + + + + + + + + + + + + + +