diff --git a/package-lock.json b/package-lock.json index 2bcc9b8..134d859 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@testing-library/user-event": "^13.5.0", "axios": "^1.4.0", "bootstrap": "^5.2.3", + "dayjs": "^1.11.9", + "jwt-decode": "^3.1.2", "react": "^18.2.0", "react-bootstrap": "^2.7.4", "react-dom": "^18.2.0", @@ -6590,6 +6592,11 @@ "node": ">=10" } }, + "node_modules/dayjs": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", + "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -11837,6 +11844,11 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", diff --git a/package.json b/package.json index c8cb977..f47938a 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "@testing-library/user-event": "^13.5.0", "axios": "^1.4.0", "bootstrap": "^5.2.3", + "dayjs": "^1.11.9", + "jwt-decode": "^3.1.2", "react": "^18.2.0", "react-bootstrap": "^2.7.4", "react-dom": "^18.2.0", diff --git a/src/App.js b/src/App.js index 667ccf2..594831e 100644 --- a/src/App.js +++ b/src/App.js @@ -1,12 +1,14 @@ import logo from "./logo.svg"; import "./App.css"; -import { Route, Routes } from "react-router-dom"; +import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import Image from "react-bootstrap/Image"; import IddrsImg from "./Static/Imgs/IDDRS.png"; import Container from "react-bootstrap/esm/Container"; import AppContext from "./AppContext"; - +import PrivateRoute from "./Utiles/PrivateRoute"; import Navbar_ from "./Components/Navbar/Navbar_"; +import { AuthProvider } from "./Context/AuthContext"; +import { URLProvider } from "./Context/URLContext"; import Home from "./Pages/Home"; import Levels from "./Pages/Levels"; @@ -14,26 +16,48 @@ import Content from "./Pages/Content"; import NewParagraph from "./Pages/NewParagraph"; import Standards from "./Pages/Standards"; import Details from "./Pages/Details"; +import Login from "./Pages/Login"; function App() { - const iddrs_url = "http://localhost:8000/admin_api"; - return (
- - - IDDRS - - - } /> - } /> - } /> - } /> - } /> - } /> - - - + + + + IDDRS + + + }> + } /> + + }> + } /> + + }> + } /> + + }> + } /> + + }> + } /> + + } + > + } + /> + + + } /> + + + +
); } diff --git a/src/Components/Content/ContentComp.js b/src/Components/Content/ContentComp.js index e312300..1992bc4 100644 --- a/src/Components/Content/ContentComp.js +++ b/src/Components/Content/ContentComp.js @@ -1,12 +1,13 @@ import React, { useContext, useState, useEffect } from "react"; -import AppContext from "../../AppContext"; +import URLContext from "../../Context/URLContext"; import Form from "react-bootstrap/Form"; import Container from "react-bootstrap/Container"; import axios from "axios"; import ContentList from "./ContentList"; +import useAxios from "../../Utiles/useAxios"; const ContentComp = () => { - const url = useContext(AppContext); + let { url } = useContext(URLContext); const [levels, setLevels] = useState([]); const [standards, setStandards] = useState([]); @@ -19,12 +20,19 @@ const ContentComp = () => { const [data, setData] = useState([]); - useEffect(() => { - axios - .get(url + "/levels/") - .then((response) => setLevels(response.data)) - .catch((error) => console.log(error)); - }, []); + let api = useAxios(); + + let getLevels = async () => { + let response = await api.get("/levels/"); + if (response.status === 200) { + setLevels(response.data); + } + }; + + useEffect(()=> { + getLevels() +}, []) + useEffect(() => { axios @@ -60,7 +68,6 @@ const ContentComp = () => { return ( - { ))} )} - + ); }; diff --git a/src/Components/HomePage/HomePage.js b/src/Components/HomePage/HomePage.js index 6c9183f..e7bc8d4 100644 --- a/src/Components/HomePage/HomePage.js +++ b/src/Components/HomePage/HomePage.js @@ -1,5 +1,5 @@ import React, { useContext, useState, useEffect } from "react"; -import AppContext from "../../AppContext"; +import URLContext from "../../Context/URLContext"; import Button from "react-bootstrap/esm/Button"; import axios from "axios"; import Row from "react-bootstrap/Row"; @@ -8,7 +8,7 @@ import Classes from "./HomePage.module.css"; import ClipLoader from "react-spinners/ClipLoader"; const HomePage = () => { - const url = useContext(AppContext); + let {url} = useContext(URLContext) const [newContentTracker, setNewContentTracker] = useState([]); const [newContents, setNewContents] = useState(true); const [isLoading, setIsLoading] = useState(false); diff --git a/src/Components/LevelStandard/LevelStandard.js b/src/Components/LevelStandard/LevelStandard.js index 2933947..49da71a 100644 --- a/src/Components/LevelStandard/LevelStandard.js +++ b/src/Components/LevelStandard/LevelStandard.js @@ -1,10 +1,10 @@ import React, { useContext, useState, useEffect } from "react"; -import AppContext from "../../AppContext"; +import URLContext from "../../Context/URLContext"; import axios from "axios"; import Form from "react-bootstrap/Form"; const LevelStandard = ({ onValueChange }) => { - const url = useContext(AppContext); + let {url} = useContext(URLContext) const [levels, setLevels] = useState([]); const [standards, setStandards] = useState([]); diff --git a/src/Components/LevelsList/LevelsList.js b/src/Components/LevelsList/LevelsList.js index 9345d5d..00d87e4 100644 --- a/src/Components/LevelsList/LevelsList.js +++ b/src/Components/LevelsList/LevelsList.js @@ -1,5 +1,5 @@ import React, { useContext, useState, useEffect } from "react"; -import AppContext from "../../AppContext"; +import URLContext from "../../Context/URLContext"; import axios from "axios"; import Button from "react-bootstrap/Button"; import Table from "react-bootstrap/Table"; @@ -8,7 +8,7 @@ import Row from "react-bootstrap/Row"; import Col from "react-bootstrap/Col"; const LevelsList = () => { - const url = useContext(AppContext); + let { url } = useContext(URLContext); const [editingRow, setEditingRow] = useState(null); const [levels, setLevels] = useState([]); @@ -32,7 +32,6 @@ const LevelsList = () => { }) .then((response) => { console.log("Form submitted successfully:", response.data); - }) .catch((error) => { console.error("Error submitting form:", error); diff --git a/src/Components/LoginPage/LoginPage.js b/src/Components/LoginPage/LoginPage.js new file mode 100644 index 0000000..8f0edd5 --- /dev/null +++ b/src/Components/LoginPage/LoginPage.js @@ -0,0 +1,35 @@ +import React, { useContext } from "react"; +import Form from "react-bootstrap/Form"; +import Button from "react-bootstrap/Button"; +import AuthContext from "../../Context/AuthContext"; + +const LoginPage = () => { + let { loginUser } = useContext(AuthContext); + return ( +
+
+ + Email address + + + + Password + + + +
+
+ ); +}; + +export default LoginPage; diff --git a/src/Components/Navbar/Navbar_.js b/src/Components/Navbar/Navbar_.js index 4a7553c..d9e4d5d 100644 --- a/src/Components/Navbar/Navbar_.js +++ b/src/Components/Navbar/Navbar_.js @@ -1,11 +1,14 @@ -import React from "react"; +import React, {useContext} from "react"; import Nav from "react-bootstrap/Nav"; import Navbar from "react-bootstrap/Navbar"; import Container from "react-bootstrap/esm/Container"; import { NavLink } from "react-router-dom"; import classes from "./Navbar_.module.css"; +import AuthContext from "../../Context/AuthContext"; +import Button from "react-bootstrap/esm/Button"; const Navbar_ = () => { + let {user, logoutUser} = useContext(AuthContext); return ( @@ -52,6 +55,13 @@ const Navbar_ = () => { > Standards + + {user &&

Hello { user.username }

} + + {user && ( + + )} +
diff --git a/src/Components/NewParagraphForm/NewParagraphForm.js b/src/Components/NewParagraphForm/NewParagraphForm.js index 751a577..4c079a0 100644 --- a/src/Components/NewParagraphForm/NewParagraphForm.js +++ b/src/Components/NewParagraphForm/NewParagraphForm.js @@ -1,5 +1,5 @@ import React, { useContext, useState, useEffect } from "react"; -import AppContext from "../../AppContext"; +import URLContext from "../../Context/URLContext"; import axios from "axios"; import LevelStandard from "../LevelStandard/LevelStandard"; import Form from "react-bootstrap/Form"; @@ -9,7 +9,7 @@ import Container from "react-bootstrap/esm/Container"; import Button from "react-bootstrap/Button"; const NewParagraphForm = () => { - const url = useContext(AppContext); + let {url} = useContext(URLContext) const [selectedLevel, setSelectedLevel] = useState(""); const [selectedStandard, setSelectedStandard] = useState(""); diff --git a/src/Components/Paragraph/ParagraphDetails.js b/src/Components/Paragraph/ParagraphDetails.js index 2519579..715d49c 100644 --- a/src/Components/Paragraph/ParagraphDetails.js +++ b/src/Components/Paragraph/ParagraphDetails.js @@ -1,6 +1,6 @@ import React, { useEffect, useState, useContext } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import AppContext from "../../AppContext"; +import URLContext from "../../Context/URLContext"; import axios from "axios"; import Form from "react-bootstrap/Form"; @@ -10,7 +10,7 @@ import Container from "react-bootstrap/esm/Container"; import Button from "react-bootstrap/Button"; const ParagraphDetails = ({ selectedParagraph }) => { - const url = useContext(AppContext); + let {url} = useContext(URLContext) const navigate = useNavigate(); const { level, standard, pk } = useParams(); diff --git a/src/Components/StandardsList/StandardsList.js b/src/Components/StandardsList/StandardsList.js index 2df226d..8af21ae 100644 --- a/src/Components/StandardsList/StandardsList.js +++ b/src/Components/StandardsList/StandardsList.js @@ -1,5 +1,5 @@ import React, { useContext, useState, useEffect } from "react"; -import AppContext from "../../AppContext"; +import URLContext from "../../Context/URLContext"; import axios from "axios"; import Button from "react-bootstrap/Button"; import Table from "react-bootstrap/Table"; @@ -9,7 +9,7 @@ import Col from "react-bootstrap/Col"; import LevelsList from "../LevelsList/LevelsList"; const StandardsList = () => { - const url = useContext(AppContext); + let {url} = useContext(URLContext) const [standards, setStandards] = useState([]); const [levels, setLevels] = useState([]); const [editingRow, setEditingRow] = useState(null); diff --git a/src/Context/AuthContext.js b/src/Context/AuthContext.js new file mode 100644 index 0000000..2e38272 --- /dev/null +++ b/src/Context/AuthContext.js @@ -0,0 +1,96 @@ +import { createContext, useEffect, useState, useContext } from "react"; +import URLContext from "./URLContext"; +import jwt_decode from "jwt-decode"; +import { useNavigate } from "react-router-dom"; + +const AuthContext = createContext(); + +export default AuthContext; + +export const AuthProvider = ({ children }) => { + let { url } = useContext(URLContext); + + let [user, setUser] = useState(() => + localStorage.getItem("authTokens") + ? jwt_decode(localStorage.getItem("authTokens")) + : null + ); + + let [authTokens, setAuthTokens] = useState(() => + localStorage.getItem("authTokens") + ? JSON.parse(localStorage.getItem("authTokens")) + : null + ); + + let [loading, setLoading] = useState(true); + + let navigate = useNavigate(); + + let loginUser = async (e) => { + e.preventDefault(); + + let response = await fetch(url + "/api/token/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: e.target.username.value, + password: e.target.password.value, + }), + }); + let data = await response.json(); + if (response.status === 200) { + setAuthTokens(data); + setUser(jwt_decode(data.access)); + localStorage.setItem("authTokens", JSON.stringify(data)); + navigate("/"); + } else { + alert("Login Failed"); + } + }; + + let logoutUser = () => { + setUser(null); + setAuthTokens(null); + localStorage.removeItem("authTokens"); + navigate("/login"); + }; + + //let updateToken = async () => { + // let response = await fetch(url + "/api/token/refresh/", { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + // body: JSON.stringify({ + // refresh: authTokens.refresh, + // }), + // }); + // let data = await response.json(); + // if (response.status === 200) { + // setAuthTokens(data); + // setUser(jwt_decode(data.access)); + // localStorage.setItem("authTokens", JSON.stringify(data)); + // } else { + // logoutUser(); + // } + //}; +// + useEffect(() => { + if (authTokens) { + setUser(jwt_decode(authTokens.access)) + } + setLoading(false); + }, [authTokens, loading]); + + let contextData = { + user: user, + authTokens: authTokens, + loginUser: loginUser, + logoutUser: logoutUser, + }; + return ( + {children} + ); +}; diff --git a/src/Context/URLContext.js b/src/Context/URLContext.js new file mode 100644 index 0000000..d364551 --- /dev/null +++ b/src/Context/URLContext.js @@ -0,0 +1,14 @@ +import { createContext, useEffect, useState } from "react"; + +const URLContext = createContext(); + +export default URLContext; + +export const URLProvider = ({ children }) => { + const url = "http://localhost:8000/admin_api"; + return ( + + {children} + + ); +}; diff --git a/src/Pages/Login.js b/src/Pages/Login.js new file mode 100644 index 0000000..1415e71 --- /dev/null +++ b/src/Pages/Login.js @@ -0,0 +1,12 @@ +import React from 'react' +import LoginPage from '../Components/LoginPage/LoginPage' +import Container from 'react-bootstrap/Container' +const Login = () => { + return ( + + + + ) +} + +export default Login \ No newline at end of file diff --git a/src/Utiles/AxiosInstence.js b/src/Utiles/AxiosInstence.js new file mode 100644 index 0000000..b4e0817 --- /dev/null +++ b/src/Utiles/AxiosInstence.js @@ -0,0 +1,35 @@ +import axios from "axios"; +import jwt_decode from "jwt-decode"; +import dayjs from "dayjs"; + +let baseURL = "http://localhost:8000/admin_api"; + +let authTokens = localStorage.getItem('authTokens') ? JSON.parse(localStorage.getItem('authTokens')) : null + +const axiosInstance = axios.create({ + baseURL, + headers:{Authorization: `Bearer ${authTokens?.access}`} +}); + +axiosInstance.interceptors.request.use(async req => { + if(!authTokens){ + authTokens = localStorage.getItem('authTokens') ? JSON.parse(localStorage.getItem('authTokens')) : null + req.headers.Authorization = `Bearer ${authTokens?.access}` + } + + const user = jwt_decode(authTokens.access) + const isExpired = dayjs.unix(user.exp).diff(dayjs()) < 1; + + if(!isExpired) return req + + const response = await axios.post(`${baseURL}/api/token/refresh/`, { + refresh: authTokens.refresh + }); + + localStorage.setItem('authTokens', JSON.stringify(response.data)) + req.headers.Authorization = `Bearer ${response.data.access}` + return req +}) + + +export default axiosInstance; \ No newline at end of file diff --git a/src/Utiles/PrivateRoute.js b/src/Utiles/PrivateRoute.js new file mode 100644 index 0000000..46cb33e --- /dev/null +++ b/src/Utiles/PrivateRoute.js @@ -0,0 +1,11 @@ +import { Route, Navigate, Outlet } from "react-router-dom"; +import { useContext } from "react"; +import AuthContext from "../Context/AuthContext"; + +const PrivateRoute = ({ children, ...rest }) => { + let { user } = useContext(AuthContext); + + return user ? : ; +}; + +export default PrivateRoute; diff --git a/src/Utiles/useAxios.js b/src/Utiles/useAxios.js new file mode 100644 index 0000000..75c303a --- /dev/null +++ b/src/Utiles/useAxios.js @@ -0,0 +1,43 @@ +import axios from 'axios' +import jwt_decode from "jwt-decode"; +import dayjs from 'dayjs' +import { useContext } from 'react' +import AuthContext from '../Context/AuthContext' + + +const baseURL = "http://localhost:8000/admin_api"; + + +const useAxios = () => { + const {authTokens, setUser, setAuthTokens} = useContext(AuthContext) + + const axiosInstance = axios.create({ + baseURL, + headers:{Authorization: `Bearer ${authTokens?.access}`} + }); + + + axiosInstance.interceptors.request.use(async req => { + + const user = jwt_decode(authTokens.access) + const isExpired = dayjs.unix(user.exp).diff(dayjs()) < 1; + + if(!isExpired) return req + + const response = await axios.post(`${baseURL}/api/token/refresh/`, { + refresh: authTokens.refresh + }); + + localStorage.setItem('authTokens', JSON.stringify(response.data)) + + setAuthTokens(response.data) + setUser(jwt_decode(response.data.access)) + + req.headers.Authorization = `Bearer ${response.data.access}` + return req + }) + + return axiosInstance +} + +export default useAxios; \ No newline at end of file