commit
46d06cd463
19 changed files with 29822 additions and 0 deletions
@ -0,0 +1,2 @@ |
|||||
|
*.ico filter=lfs diff=lfs merge=lfs -text |
||||
|
*.png filter=lfs diff=lfs merge=lfs -text |
||||
@ -0,0 +1,130 @@ |
|||||
|
# Logs |
||||
|
logs |
||||
|
*.log |
||||
|
npm-debug.log* |
||||
|
yarn-debug.log* |
||||
|
yarn-error.log* |
||||
|
lerna-debug.log* |
||||
|
.pnpm-debug.log* |
||||
|
|
||||
|
# Diagnostic reports (https://nodejs.org/api/report.html) |
||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json |
||||
|
|
||||
|
# Runtime data |
||||
|
pids |
||||
|
*.pid |
||||
|
*.seed |
||||
|
*.pid.lock |
||||
|
|
||||
|
# Directory for instrumented libs generated by jscoverage/JSCover |
||||
|
lib-cov |
||||
|
|
||||
|
# Coverage directory used by tools like istanbul |
||||
|
coverage |
||||
|
*.lcov |
||||
|
|
||||
|
# nyc test coverage |
||||
|
.nyc_output |
||||
|
|
||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) |
||||
|
.grunt |
||||
|
|
||||
|
# Bower dependency directory (https://bower.io/) |
||||
|
bower_components |
||||
|
|
||||
|
# node-waf configuration |
||||
|
.lock-wscript |
||||
|
|
||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html) |
||||
|
build/Release |
||||
|
|
||||
|
# Dependency directories |
||||
|
node_modules/ |
||||
|
jspm_packages/ |
||||
|
|
||||
|
# Snowpack dependency directory (https://snowpack.dev/) |
||||
|
web_modules/ |
||||
|
|
||||
|
# TypeScript cache |
||||
|
*.tsbuildinfo |
||||
|
|
||||
|
# Optional npm cache directory |
||||
|
.npm |
||||
|
|
||||
|
# Optional eslint cache |
||||
|
.eslintcache |
||||
|
|
||||
|
# Optional stylelint cache |
||||
|
.stylelintcache |
||||
|
|
||||
|
# Microbundle cache |
||||
|
.rpt2_cache/ |
||||
|
.rts2_cache_cjs/ |
||||
|
.rts2_cache_es/ |
||||
|
.rts2_cache_umd/ |
||||
|
|
||||
|
# Optional REPL history |
||||
|
.node_repl_history |
||||
|
|
||||
|
# Output of 'npm pack' |
||||
|
*.tgz |
||||
|
|
||||
|
# Yarn Integrity file |
||||
|
.yarn-integrity |
||||
|
|
||||
|
# dotenv environment variable files |
||||
|
.env |
||||
|
.env.development.local |
||||
|
.env.test.local |
||||
|
.env.production.local |
||||
|
.env.local |
||||
|
|
||||
|
# parcel-bundler cache (https://parceljs.org/) |
||||
|
.cache |
||||
|
.parcel-cache |
||||
|
|
||||
|
# Next.js build output |
||||
|
.next |
||||
|
out |
||||
|
|
||||
|
# Nuxt.js build / generate output |
||||
|
.nuxt |
||||
|
dist |
||||
|
|
||||
|
# Gatsby files |
||||
|
.cache/ |
||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js |
||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support |
||||
|
# public |
||||
|
|
||||
|
# vuepress build output |
||||
|
.vuepress/dist |
||||
|
|
||||
|
# vuepress v2.x temp and cache directory |
||||
|
.temp |
||||
|
.cache |
||||
|
|
||||
|
# Docusaurus cache and generated files |
||||
|
.docusaurus |
||||
|
|
||||
|
# Serverless directories |
||||
|
.serverless/ |
||||
|
|
||||
|
# FuseBox cache |
||||
|
.fusebox/ |
||||
|
|
||||
|
# DynamoDB Local files |
||||
|
.dynamodb/ |
||||
|
|
||||
|
# TernJS port file |
||||
|
.tern-port |
||||
|
|
||||
|
# Stores VSCode versions used for testing VSCode extensions |
||||
|
.vscode-test |
||||
|
|
||||
|
# yarn v2 |
||||
|
.yarn/cache |
||||
|
.yarn/unplugged |
||||
|
.yarn/build-state.yml |
||||
|
.yarn/install-state.gz |
||||
|
.pnp.* |
||||
@ -0,0 +1,9 @@ |
|||||
|
# React-Django JWT Authentication - Frontend |
||||
|
|
||||
|
### `npm start` |
||||
|
|
||||
|
Runs the app in the development mode.\ |
||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in your browser. |
||||
|
|
||||
|
The page will reload when you make changes.\ |
||||
|
You may also see any lint errors in the console. |
||||
File diff suppressed because it is too large
@ -0,0 +1,40 @@ |
|||||
|
{ |
||||
|
"name": "frontend", |
||||
|
"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", |
||||
|
"jwt-decode": "^3.1.2", |
||||
|
"react": "^18.2.0", |
||||
|
"react-dom": "^18.2.0", |
||||
|
"react-router-dom": "^6.8.1", |
||||
|
"react-scripts": "5.0.1", |
||||
|
"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" |
||||
|
] |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
@ -0,0 +1,43 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="utf-8" /> |
||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
||||
|
<meta name="theme-color" content="#000000" /> |
||||
|
<meta |
||||
|
name="description" |
||||
|
content="Web site created using create-react-app" |
||||
|
/> |
||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> |
||||
|
<!-- |
||||
|
manifest.json provides metadata used when your web app is installed on a |
||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ |
||||
|
--> |
||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> |
||||
|
<!-- |
||||
|
Notice the use of %PUBLIC_URL% in the tags above. |
||||
|
It will be replaced with the URL of the `public` folder during the build. |
||||
|
Only files inside the `public` folder can be referenced from the HTML. |
||||
|
|
||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will |
||||
|
work correctly both with client-side routing and a non-root public URL. |
||||
|
Learn how to configure a non-root public URL by running `npm run build`. |
||||
|
--> |
||||
|
<title>React App</title> |
||||
|
</head> |
||||
|
<body> |
||||
|
<noscript>You need to enable JavaScript to run this app.</noscript> |
||||
|
<div id="root"></div> |
||||
|
<!-- |
||||
|
This HTML file is a template. |
||||
|
If you open it directly in the browser, you will see an empty page. |
||||
|
|
||||
|
You can add webfonts, meta tags, or analytics to this file. |
||||
|
The build step will place the bundled scripts into the <body> tag. |
||||
|
|
||||
|
To begin the development, run `npm start` or `yarn start`. |
||||
|
To create a production bundle, use `npm run build` or `yarn build`. |
||||
|
--> |
||||
|
</body> |
||||
|
</html> |
||||
Binary file not shown.
Binary file not shown.
@ -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" |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
# https://www.robotstxt.org/robotstxt.html |
||||
|
User-agent: * |
||||
|
Disallow: |
||||
@ -0,0 +1,28 @@ |
|||||
|
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom' |
||||
|
|
||||
|
import { AuthProvider } from './context/AuthContext' |
||||
|
|
||||
|
import HomePage from './pages/HomePage' |
||||
|
import LoginPage from './pages/LoginPage' |
||||
|
import Header from './components/Header' |
||||
|
|
||||
|
import PrivateRoute from './utils/PrivateRoute' |
||||
|
|
||||
|
|
||||
|
function App() { |
||||
|
return ( |
||||
|
<div className="App"> |
||||
|
<Router> |
||||
|
<AuthProvider> |
||||
|
<Header/> |
||||
|
<Routes> |
||||
|
<Route path="/" element={<PrivateRoute><HomePage/></PrivateRoute>} /> |
||||
|
<Route path="/login" element={<LoginPage/>}/> |
||||
|
</Routes> |
||||
|
</AuthProvider> |
||||
|
</Router> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default App; |
||||
@ -0,0 +1,23 @@ |
|||||
|
import React, { useContext } from 'react' |
||||
|
import { Link } from 'react-router-dom' |
||||
|
import AuthContext from '../context/AuthContext' |
||||
|
|
||||
|
const Header = () => { |
||||
|
let { user, logoutUser } = useContext(AuthContext) |
||||
|
|
||||
|
return ( |
||||
|
<div> |
||||
|
<Link to="/">Home</Link> |
||||
|
<span> | </span> |
||||
|
{user ? ( |
||||
|
<span onClick={logoutUser}>Logout</span> |
||||
|
) : ( |
||||
|
<Link to="/login" >Login</Link> |
||||
|
)} |
||||
|
{user && <p>Hello {user.username}!</p>} |
||||
|
|
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Header |
||||
@ -0,0 +1,97 @@ |
|||||
|
import { createContext, useState, useEffect } from 'react' |
||||
|
import jwtDecode from 'jwt-decode'; |
||||
|
import { useNavigate } from 'react-router-dom' |
||||
|
|
||||
|
const AuthContext = createContext() |
||||
|
|
||||
|
export default AuthContext; |
||||
|
|
||||
|
export const AuthProvider = ({children}) => { |
||||
|
|
||||
|
let [user, setUser] = useState(() => (localStorage.getItem('authTokens') ? jwtDecode(localStorage.getItem('authTokens')) : null)) |
||||
|
let [authTokens, setAuthTokens] = useState(() => (localStorage.getItem('authTokens') ? JSON.parse(localStorage.getItem('authTokens')) : null)) |
||||
|
let [loading, setLoading] = useState(true) |
||||
|
|
||||
|
const navigate = useNavigate() |
||||
|
|
||||
|
let loginUser = async (e) => { |
||||
|
e.preventDefault() |
||||
|
const response = await fetch('http://127.0.0.1:8000/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(data){ |
||||
|
localStorage.setItem('authTokens', JSON.stringify(data)); |
||||
|
setAuthTokens(data) |
||||
|
setUser(jwtDecode(data.access)) |
||||
|
navigate('/') |
||||
|
} else { |
||||
|
alert('Something went wrong while logging in the user!') |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let logoutUser = () => { |
||||
|
// e.preventDefault()
|
||||
|
localStorage.removeItem('authTokens') |
||||
|
setAuthTokens(null) |
||||
|
setUser(null) |
||||
|
navigate('/login') |
||||
|
} |
||||
|
|
||||
|
const updateToken = async () => { |
||||
|
const response = await fetch('http://127.0.0.1:8000/api/token/refresh/', { |
||||
|
method: 'POST', |
||||
|
headers: { |
||||
|
'Content-Type':'application/json' |
||||
|
}, |
||||
|
body:JSON.stringify({refresh:authTokens?.refresh}) |
||||
|
}) |
||||
|
|
||||
|
const data = await response.json() |
||||
|
if (response.status === 200) { |
||||
|
setAuthTokens(data) |
||||
|
setUser(jwtDecode(data.access)) |
||||
|
localStorage.setItem('authTokens',JSON.stringify(data)) |
||||
|
} else { |
||||
|
logoutUser() |
||||
|
} |
||||
|
|
||||
|
if(loading){ |
||||
|
setLoading(false) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let contextData = { |
||||
|
user:user, |
||||
|
authTokens:authTokens, |
||||
|
loginUser:loginUser, |
||||
|
logoutUser:logoutUser, |
||||
|
} |
||||
|
|
||||
|
useEffect(()=>{ |
||||
|
if(loading){ |
||||
|
updateToken() |
||||
|
} |
||||
|
|
||||
|
const REFRESH_INTERVAL = 1000 * 60 * 4 // 4 minutes
|
||||
|
let interval = setInterval(()=>{ |
||||
|
if(authTokens){ |
||||
|
updateToken() |
||||
|
} |
||||
|
}, REFRESH_INTERVAL) |
||||
|
return () => clearInterval(interval) |
||||
|
|
||||
|
},[authTokens, loading]) |
||||
|
|
||||
|
return( |
||||
|
<AuthContext.Provider value={contextData}> |
||||
|
{children} |
||||
|
</AuthContext.Provider> |
||||
|
) |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
body { |
||||
|
margin: 0; |
||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', |
||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', |
||||
|
sans-serif; |
||||
|
-webkit-font-smoothing: antialiased; |
||||
|
-moz-osx-font-smoothing: grayscale; |
||||
|
} |
||||
|
|
||||
|
code { |
||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', |
||||
|
monospace; |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import React from 'react'; |
||||
|
import ReactDOM from 'react-dom/client'; |
||||
|
import './index.css'; |
||||
|
import App from './App'; |
||||
|
|
||||
|
const root = ReactDOM.createRoot(document.getElementById('root')); |
||||
|
root.render( |
||||
|
<React.StrictMode> |
||||
|
<App /> |
||||
|
</React.StrictMode> |
||||
|
); |
||||
@ -0,0 +1,38 @@ |
|||||
|
import React, { useState, useEffect, useContext } from 'react' |
||||
|
import AuthContext from '../context/AuthContext'; |
||||
|
|
||||
|
const HomePage = () => { |
||||
|
const { authTokens, logoutUser } = useContext(AuthContext); |
||||
|
let [profile, setProfile] = useState([]) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
getProfile() |
||||
|
},[]) |
||||
|
|
||||
|
const getProfile = async() => { |
||||
|
let response = await fetch('http://127.0.0.1:8000/api/profile', { |
||||
|
method: 'GET', |
||||
|
headers:{ |
||||
|
'Content-Type': 'application/json', |
||||
|
'Authorization':'Bearer ' + String(authTokens.access) |
||||
|
} |
||||
|
}) |
||||
|
let data = await response.json() |
||||
|
console.log(data) |
||||
|
if(response.status === 200){ |
||||
|
setProfile(data) |
||||
|
} else if(response.statusText === 'Unauthorized'){ |
||||
|
logoutUser() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div> |
||||
|
<p>You are logged in to the homepage!</p> |
||||
|
<p>Name: {profile.first_name} {profile.last_name}</p> |
||||
|
<p>Email: {profile.email}</p> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default HomePage |
||||
@ -0,0 +1,19 @@ |
|||||
|
import React, {useContext} from 'react' |
||||
|
import AuthContext from '../context/AuthContext' |
||||
|
|
||||
|
const LoginPage = () => { |
||||
|
|
||||
|
let {loginUser} = useContext(AuthContext) |
||||
|
|
||||
|
return ( |
||||
|
<div> |
||||
|
<form onSubmit={loginUser}> |
||||
|
<input type="text" name="username" placeholder="Enter username"/> |
||||
|
<input type="password" name="password" placeholder="enter password"/> |
||||
|
<input type="submit"/> |
||||
|
</form> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default LoginPage |
||||
@ -0,0 +1,11 @@ |
|||||
|
import { Navigate } from 'react-router-dom' |
||||
|
import { useContext } from 'react' |
||||
|
import AuthContext from '../context/AuthContext'; |
||||
|
|
||||
|
const PrivateRoute = ({children, ...rest}) => { |
||||
|
let { user } = useContext(AuthContext) |
||||
|
|
||||
|
return !user ? <Navigate to='/login'/> : children; |
||||
|
} |
||||
|
|
||||
|
export default PrivateRoute; |
||||
Loading…
Reference in new issue