diff --git a/src/App.tsx b/src/App.tsx index 9dfb6af6..c0f0f5e2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,49 +14,49 @@ import ProfilePage from "./shared/pages/Profile.js"; import LoginRedirection from "./auth/Login.tsx"; import LogoutRedirection from "./auth/Logout.tsx"; import StickyFooter from "./shared/components/Navigation/StickyFooter.tsx"; -import IsAuthenticated from "./auth/Auth.tsx"; import Token from "./auth/Token.tsx"; import { HelmetProvider } from 'react-helmet-async'; +import { AuthProvider } from './context/AuthContext.tsx'; function App() { - const authenticated = IsAuthenticated(); - return ( - -
- -
- - } /> - App is Healthy

} /> - } /> - } /> - } /> - } /> - } /> + + +
+ +
+ + } /> + App is Healthy

} /> + } /> + } /> + } /> + } /> + } /> - } /> - } /> - } - /> - } /> - } /> - } /> - } - /> - } /> + } /> + } /> + } + /> + } /> + } /> + } /> + } + /> + } /> - } /> -
-
- -
-
+ } /> +
+
+ +
+
+ ); } diff --git a/src/auth/Auth.tsx b/src/auth/Auth.tsx deleted file mode 100644 index 1f68deda..00000000 --- a/src/auth/Auth.tsx +++ /dev/null @@ -1,11 +0,0 @@ -const IsAuthenticated = (): [string, boolean] => { - const jwt = localStorage.getItem('jwt'); - - if (!jwt) { - return ["", false]; - } - - return [jwt, true]; -}; - -export default IsAuthenticated; \ No newline at end of file diff --git a/src/auth/Login.tsx b/src/auth/Login.tsx index 6f78c949..9622236c 100644 --- a/src/auth/Login.tsx +++ b/src/auth/Login.tsx @@ -1,6 +1,10 @@ -const LoginRedirection = () => { - window.location.href = `${process.env.REACT_APP_BACKEND_SERVER}/login`; - return null; // No need to render anything, as the redirection happens immediately -}; +import { useAuth } from "../context/AuthContext.tsx"; -export default LoginRedirection; \ No newline at end of file +export default function LoginRedirection() { + const { auth } = useAuth(); + if (auth.isAuthenticated) { + window.location.href = "/"; + } + window.location.href = `${process.env.REACT_APP_BACKEND_SERVER}/login`; + return null; +}; \ No newline at end of file diff --git a/src/auth/Logout.tsx b/src/auth/Logout.tsx index da4aeeb7..1d31e828 100644 --- a/src/auth/Logout.tsx +++ b/src/auth/Logout.tsx @@ -1,47 +1,38 @@ import { useEffect } from "react"; +import { useAuth } from "../context/AuthContext.tsx"; -const logout = async (token: string) => { - try { - const response = await fetch( - `${process.env.REACT_APP_BACKEND_SERVER}/logout`, - { - method: "GET", - credentials: "include", // to send cookies or session data - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); - if (response.ok) { - // Clear local storage or tokens - localStorage.removeItem("jwt"); - - // Redirect to the homepage or login page - const intervalId = setInterval(() => { - if (!localStorage.getItem("jwt")) { - clearInterval(intervalId); // Clear the interval once condition is met - window.location.href = "/"; - } - }, 5); // Check every 5ms (adjust as needed) - } else { - console.error("Failed to logout"); - } - } catch (error) { - console.error("Error logging out:", error); - window.location.href = "/"; - } -}; +export default function LogoutRedirection() { + const { auth, logout } = useAuth(); -const LogoutRedirection = (authenticated) => { - if (!authenticated.authenticated[1]) { + if (!auth.isAuthenticated) { window.location.href = "/"; } console.log("Logging out..."); useEffect(() => { - logout(authenticated.authenticated[0]); - }, []); // Run only on component mount + async function logoutUser() { + try { + const response = await fetch( + `${process.env.REACT_APP_BACKEND_SERVER}/logout`, + { + method: "GET", + credentials: "include", // to send cookies or session data + headers: { + Authorization: `Bearer ${auth.token}`, + }, + } + ); + if (response.ok) { + logout(); + } else { + console.error("Failed to logout"); + } + } catch (error) { + console.error("Error logging out:", error); + window.location.href = "/"; + } + } + logoutUser(); + }, [auth, logout]); return null; // Since this component doesn't need to render anything -}; - -export default LogoutRedirection; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/auth/Token.tsx b/src/auth/Token.tsx index 05cf3015..515f5d87 100644 --- a/src/auth/Token.tsx +++ b/src/auth/Token.tsx @@ -1,9 +1,12 @@ -const Token = () => { +import { useAuth } from "../context/AuthContext.tsx"; + +export default function Token() { + + const { login } = useAuth(); const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get("code"); - console.log("Code:", code); if (code) { fetch(`${process.env.REACT_APP_BACKEND_SERVER}/token`, { method: "POST", @@ -14,26 +17,16 @@ const Token = () => { }) .then((response) => response.json()) .then((data) => { - console.log("Data:", data); const token = data.token; - console.log("Token:", token); if (token) { - localStorage.setItem("jwt", token); - - // Periodically check if both jwt and jwt-time are set - const intervalId = setInterval(() => { - if (localStorage.getItem("jwt")) { - clearInterval(intervalId); // Clear the interval once condition is met - window.location.href = "/"; - } - }, 5); // Check every 5ms (adjust as needed) + login(token); return null; } }) .catch((error) => console.error("Error fetching token:", error)); + } else { + window.location.href = "/"; } return null; -} - -export default Token; \ No newline at end of file +} \ No newline at end of file diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 00000000..add455b5 --- /dev/null +++ b/src/context/AuthContext.tsx @@ -0,0 +1,53 @@ +import React, { createContext, useState, useContext } from 'react'; + +const AuthContext = createContext<{ + auth: { isAuthenticated: boolean; token: string | null }; + login: (token: string) => void; + logout: () => void; + loadToken: () => void; +}>({ + auth: { isAuthenticated: false, token: null }, + login: () => { }, + logout: () => { }, + loadToken: () => { } +}); + +import { ReactNode } from 'react'; + +interface AuthProviderProps { + children: ReactNode; +} + +export const AuthProvider = ({ children }: AuthProviderProps) => { + const [auth, setAuth] = useState<{ isAuthenticated: boolean; token: string | null }>({ + isAuthenticated: false, + token: null, + }); + + const login = (token: string) => { + setAuth({ isAuthenticated: true, token }); + // Save token to localStorage for persistence + localStorage.setItem('jwt', token); + }; + + const logout = () => { + setAuth({ isAuthenticated: false, token: null }); + // Clear token from localStorage + localStorage.removeItem('jwt'); + }; + + const loadToken = () => { + const savedToken = localStorage.getItem('jwt'); + if (savedToken) { + setAuth({ isAuthenticated: true, token: savedToken }); + } + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => useContext(AuthContext); \ No newline at end of file diff --git a/src/context/global/GlobalContext.js b/src/context/global/GlobalContext.js deleted file mode 100644 index 2a010079..00000000 --- a/src/context/global/GlobalContext.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createContext } from 'react'; - -const globalContext = createContext(null); - -export default globalContext; diff --git a/src/context/global/GlobalContextProvider.js b/src/context/global/GlobalContextProvider.js deleted file mode 100644 index 95588aa3..00000000 --- a/src/context/global/GlobalContextProvider.js +++ /dev/null @@ -1,57 +0,0 @@ -import React, { useEffect } from "react"; -import { Cookies, useCookies } from "react-cookie"; -import globalContext from "./GlobalContext"; -import { useReducer } from "react"; -import useGlobalContext from "./useGlobalContext"; - -// log in function - -const GlobalContextProvider = ({ children }) => { - // unpack cookies - const [cookies, setCookie, removeCookie] = useCookies(["userCookie"]); - - const unpackCookies = () => { - const user = cookies.userCookie || null; - - if (!user) { - // if no user cookie, create one - const userInfo = { - loggedIn: false, - }; - setCookie("userCookie", userInfo, { - path: "/" - }); - return userInfo; - } else { - return user; - } - }; - - function reducer(state, action) { - switch (action.type) { - case "login": - return { ...state, loggedIn: true, ...action.payload }; - case "logout": - return { loggedIn: false, ...action.payload }; - default: - return state; - } - } - - const [state, dispatch] = useReducer(reducer, unpackCookies()); - - useEffect(() => { - // login(state); - }, []); - - - - return ( - - {children} - - ); -}; - -// export provider and dispatch -export { GlobalContextProvider }; diff --git a/src/context/global/authActions.js b/src/context/global/authActions.js deleted file mode 100644 index 238a4b58..00000000 --- a/src/context/global/authActions.js +++ /dev/null @@ -1,57 +0,0 @@ -import useGlobalContext from "./useGlobalContext"; -import { useCookies } from "react-cookie"; - -const useAuthActions = () => { - const { dispatch } = useGlobalContext(); - const [cookie, setCookie, removeCookie] = useCookies(["userCookie"]); - - async function login() { - // make fake call to server - - async function getDetails() { - const response = await fetch( - `${process.env.REACT_APP_BACKEND_SERVER}/getProfessorCookies/cenzar`, - ); - - if (!response.ok) { - return; - } else { - return response.json(); - } - - // return new Promise((resolve, reject) => { - // setTimeout(() => { - // resolve({ - // loggedIn: true, - // id: "d1", - // name: "John Doe", - // email: "johnd@rpi.edu", - // role: "admin", - // department: "Computer Science", - // researchCenter: "AI", - // }); - // }, 500); - // }); - } - - const response = await getDetails(); - - // if response, dispatch and set cookies - setCookie("userCookie", response, { - path: "/", - }); - - response && dispatch({ type: "login", payload: response }); - } - - const logout = () => { - removeCookie("userCookie", { - path: "/", - }); - dispatch({ type: "logout", payload: { dispatch } }); - }; - - return { login, logout }; -}; - -export default useAuthActions; diff --git a/src/context/global/useGlobalContext.js b/src/context/global/useGlobalContext.js deleted file mode 100644 index b856f1c6..00000000 --- a/src/context/global/useGlobalContext.js +++ /dev/null @@ -1,8 +0,0 @@ -import globalContext from "./GlobalContext"; -import { useContext } from "react"; - -const useGlobalContext = () => { - return useContext(globalContext); -}; - -export default useGlobalContext; \ No newline at end of file diff --git a/src/shared/data/locations.ts b/src/shared/data/locations.ts new file mode 100644 index 00000000..4cdd4c45 --- /dev/null +++ b/src/shared/data/locations.ts @@ -0,0 +1,25 @@ +export const Locations = [ + "TBD", + "Amos Eaton", + "Carnegie", + "Center for Biotechnology and Interdisciplinary Studies", + "Center for Computational Innovations", + "Low Center for Industrial Innovation (CII)", + "Cogswell Laboratory", + "Darrin Communications Center", + "Experimental Media and Performing Arts Center", + "Greene Library", + "Jonsson Engineering Center", + "Jonsson-Rowland Science Center", + "Lally Hall", + "LINAC Facility (Gaerttner Laboratory)", + "Materials Research Center", + "Pittsburgh Building", + "Ricketts Building", + "Russell Sage Laboratory", + "Voorhees Computing Center", + "Walker Laboratory", + "West Hall", + "Winslow Building", + "Remote" +] \ No newline at end of file diff --git a/src/staff/components/CreationForms.tsx b/src/staff/components/CreationForms.tsx index 67e48693..3a202853 100644 --- a/src/staff/components/CreationForms.tsx +++ b/src/staff/components/CreationForms.tsx @@ -4,80 +4,23 @@ import { useEffect } from "react"; import CheckBox from "./Checkbox.tsx"; import Input from "./Input"; import { useParams } from "react-router"; +import { useAuth } from "../../context/AuthContext.tsx"; +import { Locations } from "../../shared/data/locations.ts"; + interface CreationFormsProps { edit: boolean; - token: string; } -const locations = [ - "TBD", - "Amos Eaton", - "Carnegie", - "Center for Biotechnology and Interdisciplinary Studies", - "Center for Computational Innovations", - "Low Center for Industrial Innovation (CII)", - "Cogswell Laboratory", - "Darrin Communications Center", - "Experimental Media and Performing Arts Center", - "Greene Library", - "Jonsson Engineering Center", - "Jonsson-Rowland Science Center", - "Lally Hall", - "LINAC Facility (Gaerttner Laboratory)", - "Materials Research Center", - "Pittsburgh Building", - "Ricketts Building", - "Russell Sage Laboratory", - "Voorhees Computing Center", - "Walker Laboratory", - "West Hall", - "Winslow Building", - "Remote" -] - -const CreationForms: React.FC = ({ edit, token }) => { +export default function CreationForms({ edit }: CreationFormsProps) { + const { auth } = useAuth(); const { postID } = useParams(); const [loading, setLoading] = useState(false); const [compensationType, setCompensationType] = useState("For Pay"); // Manage the state for "For Pay" or "For Credit" const [years, setYears] = useState([]); - async function fetchEditData() { - - - const response = await fetch( - `${process.env.REACT_APP_BACKEND_SERVER}/editOpportunity/${postID}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); - if (response.ok) { - console.log("Response ok"); - const { id, title, application_due, type, hourlyPay, credits, description, recommended_experience, location, years } = await response.json(); - await Promise.all([fetchYears()]); - reset({ - id, - title, - application_due, - type, - hourlyPay, - credits, - description, - recommended_experience, - location, - years, - }); - - setLoading(false); - } else { - console.log("No response"); - setLoading("no response"); - } - } - async function fetchYears() { - const response = await fetch(`${process.env.REACT_APP_BACKEND_SERVER}/years`) + const response = await fetch(`${process.env.REACT_APP_BACKEND_SERVER}/years`); if (response.ok) { const data = await response.json(); @@ -108,23 +51,27 @@ const CreationForms: React.FC = ({ edit, token }) => { }, }); - useEffect(() => { - fetchYears(); - if (edit) { - fetchEditData(); - } else { - setLoading(false); - } - }, []); + interface FormData { + id: string; + title: string; + application_due: string; + type: string; + hourlyPay: number; + credits: string[]; + description: string; + recommended_experience: string; + location: string; + years: string[]; + } - const submitHandler = (data) => { + function submitHandler(data: FormData) { console.log({ ...data }); if (edit) { fetch(`${process.env.REACT_APP_BACKEND_SERVER}/editOpportunity/${postID}`, { method: "PUT", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${auth.token}`, }, body: JSON.stringify({ ...data }), }).then((response) => { @@ -140,14 +87,15 @@ const CreationForms: React.FC = ({ edit, token }) => { method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${auth.token}`, }, body: JSON.stringify({ ...data }), }).then((response) => { if (response.ok) { alert("Successfully created"); - const data_response = response.json() - window.location.href = `/opportunity/${data_response["id"]}`; + response.json().then((data_response) => { + window.location.href = `/opportunity/${data_response["id"]}`; + }); } else { alert("Failed to create"); console.log(response); @@ -156,6 +104,45 @@ const CreationForms: React.FC = ({ edit, token }) => { } }; + useEffect(() => { + async function fetchEditData() { + const response = await fetch( + `${process.env.REACT_APP_BACKEND_SERVER}/editOpportunity/${postID}`, { + headers: { + Authorization: `Bearer ${auth.token}`, + }, + } + ); + if (response.ok) { + const { id, title, application_due, type, hourlyPay, credits, description, recommended_experience, location, years } = await response.json(); + await Promise.all([fetchYears()]); + reset({ + id, + title, + application_due, + type, + hourlyPay, + credits, + description, + recommended_experience, + location, + years, + }); + setLoading(false); + } else { + console.log("No response"); + setLoading("no response"); + } + } + + fetchYears(); + if (edit) { + fetchEditData(); + } else { + setLoading(false); + } + }, [edit, auth.token, postID, reset]); + return loading === false && years != null ? (
{ @@ -187,7 +174,7 @@ const CreationForms: React.FC = ({ edit, token }) => { label="Location" name={"location"} type="select" - options={locations} + options={Locations} errorMessage={"Location is required"} formHook={{ ...register("location", { @@ -334,6 +321,4 @@ const CreationForms: React.FC = ({ edit, token }) => { ) : (

Loading...

); -}; - -export default CreationForms; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/staff/pages/CreatePost.tsx b/src/staff/pages/CreatePost.tsx index 9769618c..0fad0bc5 100644 --- a/src/staff/pages/CreatePost.tsx +++ b/src/staff/pages/CreatePost.tsx @@ -1,10 +1,16 @@ import React from "react"; -import PropTypes from 'prop-types'; import CreationForms from "../components/CreationForms.tsx"; import SEO from "../../shared/components/SEO.tsx"; +import { useAuth } from "../../context/AuthContext.tsx"; -const CreatePost = ({ edit, authenticated }) => { - if (!authenticated[1]) { +interface CreatePostProps { + edit: boolean; +} + +export default function CreatePost({ edit }: CreatePostProps) { + const { auth } = useAuth(); + + if (!auth.isAuthenticated) { window.location.href = "/login"; } @@ -12,16 +18,7 @@ const CreatePost = ({ edit, authenticated }) => {

{edit === true ? "Edit Research Opportunity" : "Create Research Opportunity"}

- +
); }; -CreatePost.propTypes = { - edit: PropTypes.bool.isRequired, - authenticated: PropTypes.arrayOf(PropTypes.oneOfType([ - PropTypes.string, - PropTypes.bool - ])).isRequired -}; - -export default CreatePost; \ No newline at end of file diff --git a/src/staff/pages/Department.tsx b/src/staff/pages/Department.tsx index 6866964d..9f645550 100644 --- a/src/staff/pages/Department.tsx +++ b/src/staff/pages/Department.tsx @@ -4,43 +4,45 @@ import Breadcrumb from "../../shared/components/UIElements/Breadcrumb.tsx"; import DepartmentHeading from "../components/DepartmentHeading.tsx"; import DepartmentStaff from "../components/DepartmentStaff.tsx"; import SEO from "../../shared/components/SEO.tsx"; +import { useAuth } from "../../context/AuthContext.tsx"; -const Department = (authenticated) => { - if (!authenticated.authenticated[1]) { +export default function Department() { + const { auth } = useAuth(); + + if (!auth.isAuthenticated) { window.location.href = "/login"; } const { department } = useParams(); const [departmentstate, setDepartmentstate] = useState(false); - const fetchDepartment = async () => { - const response = await fetch( - `${process.env.REACT_APP_BACKEND_SERVER}/departments/${department}`, { - headers: { - Authorization: `Bearer ${authenticated.authenticated[0]}`, - }, - } - ); - - if (!response.ok) { - setDepartmentstate("not found"); - } else { - const data = await response.json(); - // Ensure each staff member has an image property - const updatedData = { - ...data, - staff: data.staff.map((member: { id: string; name: string; role: string; image?: string }) => ({ - ...member, - image: member.image || "default-image-url" // Provide a default image URL if none exists - })) - }; - setDepartmentstate(updatedData); - } - }; - useEffect(() => { + const fetchDepartment = async () => { + const response = await fetch( + `${process.env.REACT_APP_BACKEND_SERVER}/departments/${department}`, { + headers: { + Authorization: `Bearer ${auth.token}`, + }, + } + ); + + if (!response.ok) { + setDepartmentstate("not found"); + } else { + const data = await response.json(); + // Ensure each staff member has an image property + const updatedData = { + ...data, + staff: data.staff.map((member: { id: string; name: string; role: string; image?: string }) => ({ + ...member, + image: member.image || "default-image-url" // Provide a default image URL if none exists + })) + }; + setDepartmentstate(updatedData); + } + }; fetchDepartment(); - }, []); + }, [auth.token, department]); const departmentComponents = ( <> @@ -78,5 +80,3 @@ const Department = (authenticated) => { ); }; - -export default Department; diff --git a/src/staff/pages/Departments.tsx b/src/staff/pages/Departments.tsx index d6818c67..ac6df925 100644 --- a/src/staff/pages/Departments.tsx +++ b/src/staff/pages/Departments.tsx @@ -2,9 +2,11 @@ import React, { useEffect, useState } from "react"; import DepartmentItems from "../components/DepartmentItems.tsx"; import ErrorComponent from "../../shared/components/UIElements/Error.tsx"; import SEO from "../../shared/components/SEO.tsx"; +import { useAuth } from "../../context/AuthContext.tsx"; -const Departments = (authenticated) => { - if (!authenticated.authenticated[1]) { +export default function Departments() { + const { auth } = useAuth(); + if (!auth.isAuthenticated) { window.location.href = "/login"; } @@ -12,30 +14,29 @@ const Departments = (authenticated) => { { id: string; department_id: string; title: string; image: string }[] | string | null >(null); - const fetchDepartments = async () => { - try { - const response = await fetch( - `${process.env.REACT_APP_BACKEND_SERVER}/departments`, { - headers: { - Authorization: `Bearer ${authenticated.authenticated[0]}`, - }, - } - ); - - if (!response.ok) { - throw new Error("Departments not found"); - } - - const data = await response.json(); - setDepartments(data); - } catch { - setDepartments("Error fetching departments"); - } - }; - useEffect(() => { + const fetchDepartments = async () => { + try { + const response = await fetch( + `${process.env.REACT_APP_BACKEND_SERVER}/departments`, { + headers: { + Authorization: `Bearer ${auth.token}`, + }, + } + ); + + if (!response.ok) { + throw new Error("Departments not found"); + } + + const data = await response.json(); + setDepartments(data); + } catch { + setDepartments("Error fetching departments"); + } + }; fetchDepartments(); - }, []); + }, [auth.token]); const departmentComponents = (
@@ -57,5 +58,3 @@ const Departments = (authenticated) => { ); }; - -export default Departments; diff --git a/src/staff/pages/Staff.tsx b/src/staff/pages/Staff.tsx index c4008ce6..7c4cabd2 100644 --- a/src/staff/pages/Staff.tsx +++ b/src/staff/pages/Staff.tsx @@ -4,9 +4,12 @@ import ProfileDescription from "../components/ProfileDescription.tsx"; import ProfileOpportunities from "../components/ProfileOpportunities.tsx"; import { useParams } from "react-router"; import SEO from "../../shared/components/SEO.tsx"; +import { useAuth } from "../../context/AuthContext.tsx"; -const StaffPage = (authenticated: { authenticated: [string, boolean]; }) => { - if (!authenticated.authenticated[1]) { +export default function StaffPage() { + const { auth } = useAuth(); + + if (!auth.isAuthenticated) { window.location.href = "/login"; } @@ -21,7 +24,7 @@ const StaffPage = (authenticated: { authenticated: [string, boolean]; }) => { const response = await fetch( `${process.env.REACT_APP_BACKEND_SERVER}/staff/${staffId}`, { headers: { - Authorization: `Bearer ${authenticated.authenticated[0]}`, + Authorization: `Bearer ${auth.token}`, }, } ); @@ -72,5 +75,3 @@ const StaffPage = (authenticated: { authenticated: [string, boolean]; }) => { ); }; - -export default StaffPage;