mirror of
https://github.com/TurTaskProject/TurTaskWeb.git
synced 2025-12-19 05:54:07 +01:00
Add react sample login page link with Authen API
This commit is contained in:
parent
74f3e1eada
commit
6bbe01d932
@ -10,8 +10,20 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@mui/icons-material": "^5.14.15",
|
||||
"@mui/material": "^5.14.15",
|
||||
"axios": "^1.5.1",
|
||||
"bootstrap": "^5.3.2",
|
||||
"gapi-script": "^1.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-bootstrap": "^2.9.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-google-login": "^5.2.2",
|
||||
"react-router-dom": "^6.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,24 +1,34 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
@ -26,17 +36,3 @@
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
@ -1,35 +1,49 @@
|
||||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import viteLogo from '/vite.svg'
|
||||
import './App.css'
|
||||
// import './App.css';
|
||||
// import { Routes, Route, Link } from "react-router-dom";
|
||||
// import Login from "./components/login";
|
||||
// import TestAuth from './components/testAuth';
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
// function App() {
|
||||
// return (
|
||||
// <div className="App">
|
||||
// <nav>
|
||||
// <Link className={"nav-link"} to={"/"}>Home</Link>
|
||||
// <Link className={"nav-link"} to={"/login"}>Login</Link>
|
||||
// <Link className={"nav-link"} to={"/testAuth"}>testAuth</Link>
|
||||
// </nav>
|
||||
// <Routes>
|
||||
// <Route exact path={"/login"} element={Login} />
|
||||
// <Route exact path={"/testAuth"} element={TestAuth} />
|
||||
// <Route path={"/"} render={() => <h1>This is Home page!</h1>} />
|
||||
// </Routes>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default App;
|
||||
|
||||
import './App.css';
|
||||
import {BrowserRouter, Route, Routes, Link} from "react-router-dom";
|
||||
import Login from "./components/login";
|
||||
import TestAuth from './components/testAuth';
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://vitejs.dev" target="_blank">
|
||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
<BrowserRouter>
|
||||
<div className="App">
|
||||
<nav>
|
||||
<Link className={"nav-link"} to={"/"}>Home</Link>
|
||||
<Link className={"nav-link"} to={"/login"}>Login</Link>
|
||||
<Link className={"nav-link"} to={"/testAuth"}>testAuth</Link>
|
||||
</nav>
|
||||
<Routes>
|
||||
<Route path={"/"} render={() => <h1>This is Home page!</h1>} />
|
||||
<Route path="/login" element={<Login/>}/>
|
||||
<Route path="/testAuth" element={<TestAuth/>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.jsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the Vite and React logos to learn more
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
93
frontend/src/api/axiosapi.jsx
Normal file
93
frontend/src/api/axiosapi.jsx
Normal file
@ -0,0 +1,93 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// Create an Axios instance with common configurations
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: 'http://127.0.0.1:8000/api/',
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
'Authorization': "Bearer " + localStorage.getItem('access_token'),
|
||||
'Content-Type': 'application/json',
|
||||
'accept': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
// Add a response interceptor to handle token refresh on 401 Unauthorized errors
|
||||
axiosInstance.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
const originalRequest = error.config;
|
||||
const refresh_token = localStorage.getItem('refresh_token');
|
||||
|
||||
// Check if the error is due to Unauthorized (401) and a refresh token is available
|
||||
if (error.response.status === 401 && error.response.statusText === "Unauthorized" && refresh_token !== "undefined") {
|
||||
return axiosInstance
|
||||
.post('/token/refresh/', { refresh: refresh_token })
|
||||
.then((response) => {
|
||||
// Update access and refresh tokens
|
||||
localStorage.setItem('access_token', response.data.access);
|
||||
localStorage.setItem('refresh_token', response.data.refresh);
|
||||
|
||||
// Update the authorization header with the new access token
|
||||
axiosInstance.defaults.headers['Authorization'] = "Bearer " + response.data.access;
|
||||
originalRequest.headers['Authorization'] = "Bearer " + response.data.access;
|
||||
|
||||
return axiosInstance(originalRequest); // Retry the original request
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('Interceptors error: ', err);
|
||||
});
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Function for user login
|
||||
const apiUserLogin = (data) => {
|
||||
return axiosInstance
|
||||
.post('token/obtain/', data)
|
||||
.then((response) => {
|
||||
console.log(response.statusText);
|
||||
return response;
|
||||
}).catch(error => {
|
||||
console.log('apiUserLogin error: ', error);
|
||||
return error;
|
||||
});
|
||||
};
|
||||
|
||||
// Function for user logout
|
||||
const apiUserLogout = () => {
|
||||
axiosInstance.defaults.headers['Authorization'] = ""; // Clear authorization header
|
||||
localStorage.removeItem('access_token'); // Remove access token
|
||||
localStorage.removeItem('refresh_token'); // Remove refresh token
|
||||
}
|
||||
|
||||
// Function for Google login
|
||||
const googleLogin = async (accessToken) => {
|
||||
let res = await axios.post(
|
||||
"http://localhost:8000/api/dj-rest-auth/google/",
|
||||
{
|
||||
access_token: accessToken,
|
||||
}
|
||||
);
|
||||
return await res;
|
||||
};
|
||||
|
||||
// Function to get 'hello' data
|
||||
const getGreeting = () => {
|
||||
return axiosInstance
|
||||
.get('hello')
|
||||
.then((response) => {
|
||||
return response;
|
||||
}).catch(error => {
|
||||
return error;
|
||||
});
|
||||
}
|
||||
|
||||
// Export the functions and Axios instance
|
||||
export default {
|
||||
axiosInstance,
|
||||
apiUserLogin,
|
||||
apiUserLogout,
|
||||
getGreeting: getGreeting,
|
||||
googleLogin
|
||||
};
|
||||
156
frontend/src/components/login.jsx
Normal file
156
frontend/src/components/login.jsx
Normal file
@ -0,0 +1,156 @@
|
||||
import React, { useState } from 'react';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Container from '@material-ui/core/Container';
|
||||
import axiosapi from '../api/axiosapi';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { GoogleLogin } from 'react-google-login';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
// Styles for various elements
|
||||
paper: {
|
||||
marginTop: theme.spacing(8),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
avatar: {
|
||||
margin: theme.spacing(1),
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
form: {
|
||||
width: '100%',
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
submit: {
|
||||
margin: theme.spacing(3, 0, 2),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function Login() {
|
||||
const history = useNavigate();
|
||||
const classes = useStyles();
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const handleUsernameChange = (event) => {
|
||||
// Update the 'username' state when the input field changes
|
||||
setUsername(event.target.value);
|
||||
}
|
||||
|
||||
const handleEmailChange = (event) => {
|
||||
// Update the 'email' state when the email input field changes
|
||||
setEmail(event.target.value);
|
||||
}
|
||||
|
||||
const handlePasswordChange = (event) => {
|
||||
// Update the 'password' state when the password input field changes
|
||||
setPassword(event.target.value);
|
||||
}
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
// Send a POST request to the authentication API
|
||||
axiosapi.apiUserLogin({
|
||||
email: email,
|
||||
username: username,
|
||||
password: password
|
||||
}).then(res => {
|
||||
// On successful login, store tokens and set the authorization header
|
||||
localStorage.setItem('access_token', res.data.access);
|
||||
localStorage.setItem('refresh_token', res.data.refresh);
|
||||
axiosapi.axiosInstance.defaults.headers['Authorization'] = "Bearer " + res.data.access;
|
||||
history.push('/testAuth');
|
||||
}).catch(err => {
|
||||
console.log('Login failed'); // Handle login failure
|
||||
});
|
||||
}
|
||||
|
||||
const responseGoogle = async (response) => {
|
||||
// Handle Google login response
|
||||
let googleResponse = await axiosapi.googleLogin(response.accessToken);
|
||||
console.log('Google Response:\n', googleResponse);
|
||||
|
||||
if (googleResponse.status === 200) {
|
||||
// Store Google login tokens and set the authorization header on success
|
||||
localStorage.setItem('access_token', googleResponse.data.access_token);
|
||||
localStorage.setItem('refresh_token', googleResponse.data.refresh_token);
|
||||
axiosapi.axiosInstance.defaults.headers['Authorization'] = "Bearer " + googleResponse.data.access_token;
|
||||
history.push('/testAuth');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container component="main" maxWidth="xs">
|
||||
<CssBaseline />
|
||||
<div className={classes.paper}>
|
||||
<Avatar className={classes.avatar} />
|
||||
<Typography component="h1" variant="h5">
|
||||
Sign in
|
||||
</Typography>
|
||||
<form className={classes.form} noValidate onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
id="email"
|
||||
label="Email"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
autoFocus
|
||||
onChange={handleEmailChange}
|
||||
/>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
id="username"
|
||||
label="Username"
|
||||
name="username"
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
onChange={handleUsernameChange}
|
||||
/>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<GoogleLogin
|
||||
clientId="YOUR_GOOGLE_CLIENT_ID"
|
||||
buttonText="Login"
|
||||
onSuccess={responseGoogle}
|
||||
onFailure={responseGoogle}
|
||||
cookiePolicy={'single_host_origin'}
|
||||
/>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
11
frontend/src/components/signup.jsx
Normal file
11
frontend/src/components/signup.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
function Signup() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Signup
|
||||
42
frontend/src/components/testAuth.jsx
Normal file
42
frontend/src/components/testAuth.jsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axiosapi from '../api/axiosapi';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
function TestAuth() {
|
||||
let history = useNavigate();
|
||||
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch the "hello" data from the server when the component mounts
|
||||
axiosapi.getGreeting().then(res => {
|
||||
console.log(res.data);
|
||||
setMessage(res.data.user);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
setMessage("");
|
||||
});
|
||||
}, []);
|
||||
|
||||
const logout = () => {
|
||||
// Log out the user, clear tokens, and navigate to the "/testAuth" route
|
||||
axiosapi.apiUserLogout();
|
||||
history('/testAuth');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{message !== "" && (
|
||||
<div>
|
||||
<h1>Hello!</h1>
|
||||
<h2>{message}</h2>
|
||||
<Button variant="contained" onClick={logout}>Logout</Button>
|
||||
</div>
|
||||
)}
|
||||
{message === "" && <h1>Need to sign in</h1>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TestAuth;
|
||||
@ -1,69 +1,19 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
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;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.nav-link{
|
||||
color:black;
|
||||
/* border: 1px solid white; */
|
||||
padding: 1em;
|
||||
}
|
||||
@ -1,10 +1,12 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
ReactDOM.createRoot(
|
||||
document.getElementById("root"),
|
||||
)
|
||||
.render(
|
||||
<App />
|
||||
);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user