diff --git a/frontend/src/components/AuthCommon/password-change/PasswordChange.jsx b/frontend/src/components/AuthCommon/password-change/PasswordChange.jsx new file mode 100644 index 0000000..e9323f1 --- /dev/null +++ b/frontend/src/components/AuthCommon/password-change/PasswordChange.jsx @@ -0,0 +1,275 @@ +/* eslint-disable react/prop-types */ +import React, { useEffect, useState } from "react"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import TextField from "@mui/material/TextField"; +import { isTokenExpired } from "../../../utils/auth/tokenExpiryCheck"; +import { jwtDecode } from "jwt-decode"; +import Typography from "@mui/material/Typography"; +import { updatePasswordFromProfile } from "../../../utils/api/ResetPassword/resetPassword"; + +import { useNavigation, Form, useActionData } from "react-router-dom"; + +function PasswordChange(props) { + const [passwordError, setPasswordError] = useState(false); + const [confirmPasswordError, setConfirmPasswordError] = useState(false); + const [passwordResetSuccess, setPasswordResetSuccess] = useState(false); + const [errorText, setErrorText] = useState(""); + + const actionData = useActionData(); + + useEffect(() => { + if (actionData) { + if (actionData.status == 200) { + //password reset successfully + setPasswordError(false); + setConfirmPasswordError(false); + setPasswordResetSuccess(true); + //re route to login can be done here directly but success message? + } else { + // error + setPasswordError(true); + setConfirmPasswordError(true); + setErrorText(actionData.message); + } + } + }, [actionData]); + + const navigation = useNavigation(); + + return ( + + {/* display success message if reset is success */} + <> + {passwordResetSuccess ? ( + + Password Updated Successfully + + ) : null} + + {/* password reset component */} + + Change Your Password + + +
+ {/* current password */} + + + + {/* new password */} + + + + + {/* confirm password */} + + + + + {/* dynamic error helper text */} + {passwordError || confirmPasswordError ? ( + + {errorText} + + ) : null} + + {/* submit button */} + + + +
+
+ +
+ ); +} + +export default PasswordChange; + +export async function action({ request }) { + const formData = await request.formData(); + + const current_password = formData.get("currentPassword"); + const new_password = formData.get("newPassword"); + const confirm_password = formData.get("confirmPassword"); + + const accessToken = localStorage.getItem("access"); + const tokenExpired = isTokenExpired(accessToken); + + //token expired or any issue in token + if (tokenExpired) { + const response = { + status: 500, + message: "Please login again to update password.", + }; + return response; + } + + //password match check + if (new_password != confirm_password) { + const response = { + status: 400, + message: "New Password & Current Password didn't match", + }; + return response; + } else { + //password matched and all rules followed backend call + //pass email also + const decoded = jwtDecode(accessToken); + const response = await updatePasswordFromProfile( + decoded.email, + current_password, + new_password + ); + return response; + } +} diff --git a/frontend/src/components/admin/leftNavbar/LeftNavbar.jsx b/frontend/src/components/admin/leftNavbar/LeftNavbar.jsx index d0eb350..ecad132 100644 --- a/frontend/src/components/admin/leftNavbar/LeftNavbar.jsx +++ b/frontend/src/components/admin/leftNavbar/LeftNavbar.jsx @@ -27,6 +27,7 @@ import ExpandMore from "@mui/icons-material/ExpandMore"; import StarBorder from "@mui/icons-material/StarBorder"; import "./styles.css"; import { navItemsManager } from "../../../constants/navbar"; +import { Link } from "react-router-dom"; const drawerWidth = 280; const openedMixin = (theme) => ({ @@ -278,23 +279,25 @@ export default function MiniDrawer(props) { component="li" sx={{ backgroundColor: "#ffffff", opacity: "15%" }} /> - - - {obj.icon ? ( - obj.icon - ) : ( - - )} - - + - {obj.text} - - + + {obj.icon ? ( + obj.icon + ) : ( + + )} + + + {obj.text} + + + { if (data) { //extracting skill names from skill ids - const filteredSkills = skills.filter((obj) => - data.id.skills.includes(obj.id) - ); + let filteredSkills = []; + let filteredDisciplines = []; + if (data.id.skills) { + filteredSkills = skills.filter((obj) => + data.id.skills.includes(obj.id) + ); + //extracting discipline names from discipline ids + filteredDisciplines = disciplines.filter((obj) => + data.id.skills.includes(obj.id) + ); + } const skillNames = filteredSkills.map((skill) => skill.name); - - //extracting discipline names from discipline ids - const filteredDisciplines = disciplines.filter((obj) => - data.id.skills.includes(obj.id) - ); + const disciplineNames = filteredDisciplines.map((skill) => skill.name); //application date - const dateString = data.id.last_app_date; - const parts = dateString.split(" "); - const monthIndex = - new Date(Date.parse(parts[1] + " 1, 2000")).getMonth() + 1; + let lastDate = null; + if (data.id.last_app_date) { + const dateString = data.id.last_app_date; + const parts = dateString.split(" "); + const monthIndex = + new Date(Date.parse(parts[1] + " 1, 2000")).getMonth() + 1; - const formattedDate = `${parts[2]}-${monthIndex - .toString() - .padStart(2, "0")}-${parts[0].padStart(2, "0")}`; - const lastDate = dayjs(formattedDate); + const formattedDate = `${parts[2]}-${monthIndex + .toString() + .padStart(2, "0")}-${parts[0].padStart(2, "0")}`; + lastDate = dayjs(formattedDate); + } // Update state values with data from the 'data' prop setJobDesignation(data.id.designation || ""); setMinSalary(data.id.salary_range_min || ""); setMaxSalary(data.id.salary_range_max || ""); - setJobDomain(data.id.domain.name || ""); - setJobType(data.id.job_type.jobtype || ""); - setGender(data.id.gender || ""); - setOfficeState(data.id.job_state || ""); - setOfficeCity(data.id.city_state || ""); + setJobDomain(data.id.domain ? data.id.domain.name : ""); + setJobType(data.id.job_type ? data.id.job_type.jobtype : ""); + setGender(data.id.gender ? data.id.gender : ""); + setOfficeState(data.id.job_state ? data.id.job_state : ""); + setOfficeCity(data.id.city_state ? data.id.city_state : ""); setSkillName(skillNames || []); - setJobDescription(data.id.jobDescription || ""); - setKeyResponsibilities(data.id.key_job_responsibilities || ""); - setQualification(data.id.requirements || ""); - setApplicationDate(lastDate || null); - setGradYears(data.id.gradYears || []); - setMandatorySkills(data.id.mandatorySkills || []); - setOptionalSkills(data.id.optionalSkills || []); - setDegree(data.id.degree || []); + setJobDescription(data.id.jobDescription ? data.id.jobDescription : ""); + setKeyResponsibilities( + data.id.key_job_responsibilities ? data.id.key_job_responsibilities : "" + ); + setQualification(data.id.requirements ? data.id.requirements : ""); + setApplicationDate(lastDate ? lastDate : null); + setGradYears(data.id.gradYears ? data.id.gradYears : []); + setMandatorySkills( + data.id.mandatorySkills ? data.id.mandatorySkills : [] + ); + setOptionalSkills(data.id.optionalSkills ? data.id.optionalSkills : []); + setDegree(data.id.degree ? data.id.degree : []); setDiscipline(disciplineNames || []); - setStudentLocation(data.id.studentLocation || []); + setStudentLocation( + data.id.studentLocation ? data.id.studentLocation : [] + ); } - }, [data]); + }, [data]); const handleJobDesignationChange = (event) => { setJobDesignation(event.target.value); diff --git a/frontend/src/components/company/company-job-profile/companyJobProfile.jsx b/frontend/src/components/company/company-job-profile/companyJobProfile.jsx index b4fa1c2..0104c69 100644 --- a/frontend/src/components/company/company-job-profile/companyJobProfile.jsx +++ b/frontend/src/components/company/company-job-profile/companyJobProfile.jsx @@ -4,7 +4,7 @@ import { Box, Typography } from "@mui/material"; import Divider from "@mui/material/Divider"; import JobListTable from "./jobListTable"; import { Link, defer, useLoaderData, Await } from "react-router-dom"; -import { getJobsByUserId } from "../../../utils/api/company/jobs"; +import { getJobList } from "../../../utils/api/company/jobs"; import { Suspense } from "react"; import Spinner from "../../common/Spinner"; @@ -75,5 +75,5 @@ function CompanyJobProfile() { export default CompanyJobProfile; export function loader() { const token = localStorage.getItem("access"); - return defer({ jobListData: getJobsByUserId(token) }); + return defer({ jobListData: getJobList(token) }); } diff --git a/frontend/src/components/login/ForgotPassword/SendPasswordResetLink.jsx b/frontend/src/components/login/ForgotPassword/SendPasswordResetLink.jsx index 77ec44f..192b779 100644 --- a/frontend/src/components/login/ForgotPassword/SendPasswordResetLink.jsx +++ b/frontend/src/components/login/ForgotPassword/SendPasswordResetLink.jsx @@ -57,29 +57,31 @@ export default function SendPasswordResetLink({ linkSent, linkSentError }) { {/* email input */} - + "& label.Mui-focused": { + color: "#002648", + }, + mt: "1rem", + }} + /> + ) : null} diff --git a/frontend/src/constants/navbar.js b/frontend/src/constants/navbar.js index fcbf807..5b4cf57 100644 --- a/frontend/src/constants/navbar.js +++ b/frontend/src/constants/navbar.js @@ -10,6 +10,7 @@ import FilterAltOutlinedIcon from "@mui/icons-material/FilterAltOutlined"; import ForwardToInboxOutlinedIcon from "@mui/icons-material/ForwardToInboxOutlined"; import PendingOutlinedIcon from "@mui/icons-material/PendingOutlined"; import CategoryOutlinedIcon from "@mui/icons-material/CategoryOutlined"; +import SettingsIcon from "@mui/icons-material/Settings"; import ImageOutlinedIcon from "@mui/icons-material/ImageOutlined"; import VignetteOutlinedIcon from "@mui/icons-material/VignetteOutlined"; import PageviewOutlinedIcon from "@mui/icons-material/PageviewOutlined"; @@ -33,6 +34,7 @@ const navItemsStudent = [ { text: "Dashboard", url: "dashboard" }, { text: "Jobs", url: "jobs" }, { text: "Profile", url: "profile" }, + { text: "Settings", url: "settings" }, ]; //manager dashboard @@ -165,6 +167,12 @@ const navItemsManager = [ url: "", icon: , }, + { + nested: false, + text: "Settings", + url: "settings", + icon: , + }, ]; //employer dashboard @@ -172,6 +180,7 @@ const navItemsEmployer = [ { text: "Dashboard", url: "dashboard" }, { text: "Jobs", url: "jobs" }, { text: "Profile", url: "profile" }, + { text: "Settings", url: "settings" }, ]; export { navItemsHomepage, navItemsEmployer, navItemsManager, navItemsStudent }; diff --git a/frontend/src/route/router.js b/frontend/src/route/router.js index 1e94e03..e0c2433 100644 --- a/frontend/src/route/router.js +++ b/frontend/src/route/router.js @@ -29,6 +29,11 @@ import NewPasswordInput, { action as NewPasswordInputAction, } from "../components/login/ForgotPassword/NewPasswordInput"; +//auth common +import PasswordChange, { + action as passwordChangeAction, +} from "../components/AuthCommon/password-change/PasswordChange"; + //student import StudentLayout, { action as StudentLogoutAction, @@ -120,6 +125,11 @@ const router = createBrowserRouter([ { path: "dashboard", element: }, { path: "profile", element: }, { path: "jobs", element: }, + { + path: "settings", + element: , + action: passwordChangeAction, + }, ], }, @@ -132,7 +142,14 @@ const router = createBrowserRouter([ ), action: ManagerLogoutAction, - children: [{ path: "", element: }], + children: [ + { path: "", element: }, + { + path: "settings", + element: , + action: passwordChangeAction, + }, + ], }, { @@ -147,7 +164,15 @@ const router = createBrowserRouter([ children: [ { path: "dashboard", element: }, { path: "jobs", element: , loader: jobListLoader }, - { path: "profile", element: }, + { + path: "profile", + element: , + }, + { + path: "settings", + element: , + action: passwordChangeAction, + }, ], }, { diff --git a/frontend/src/utils/api/ResetPassword/resetPassword.js b/frontend/src/utils/api/ResetPassword/resetPassword.js index 47a6835..fb23a06 100644 --- a/frontend/src/utils/api/ResetPassword/resetPassword.js +++ b/frontend/src/utils/api/ResetPassword/resetPassword.js @@ -39,7 +39,7 @@ export async function resetPassword(email) { } } -//to set new password +//to set new password from reset link export async function setNewPassword(password, token) { const url = process.env.REACT_APP_API_LINK + "/api/reset-password/" + token + "/"; @@ -76,3 +76,45 @@ export async function setNewPassword(password, token) { }; } } + +//to set new password from user's profile +export async function updatePasswordFromProfile( + email, + currentPassword, + newPassword +) { + const accessToken = localStorage.getItem("access"); + const url = process.env.REACT_APP_API_LINK + "/api/change-password/"; + const requestOptions = { + method: "POST", + headers: { + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: email, + current_password: currentPassword, + new_password: newPassword, + }), + }; + + try { + const response = await fetch(url, requestOptions); + if (!response.ok) { + const errorData = await response.json(); + return { + message: errorData.detail || errorData.error, + status: response.status, + }; + } + return { + message: "Password updated successfully", + status: response.status, + }; + } catch (error) { + return { + message: "Failed to update password", + status: 500, + }; + } +} diff --git a/frontend/src/utils/api/company/jobs.js b/frontend/src/utils/api/company/jobs.js index 3902803..b265cdb 100644 --- a/frontend/src/utils/api/company/jobs.js +++ b/frontend/src/utils/api/company/jobs.js @@ -1,26 +1,26 @@ /* eslint-disable no-undef */ //to get job list -export async function getJobsByUserId(token) { - const headers = { - 'Authorization' : 'Bearer ' + token, - 'Content-Type': 'application/json', - } +export async function getJobList(token) { try { - const options = { - method: 'GET', - headers: headers - } - const apiUrl = `${process.env.REACT_APP_API_LINK}/api/company/manager/jobs` - const response = await fetch(apiUrl, options); + const url = process.env.REACT_APP_API_LINK + "/api/company/manager/jobs/"; + + const requestOptions = { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }; + const response = await fetch(url, requestOptions); if (!response.ok) { - throw { message: "Failed to fetchs jobs", status: response.status }; + return { message: response.detail, status: response.status }; + } const jobsData = await response.json(); return jobsData; } catch (error) { - console.error("Error fetching jobs:", error); - throw error; + return { message: "Error fetching jobs:", error }; } }