@@ -15,6 +15,7 @@
|
||||
"@testing-library/react": "^13.1.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/body-parser": "^1.19.5",
|
||||
"@types/csurf": "^1.11.5",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
@@ -25,6 +26,7 @@
|
||||
"@wbr-project/wbr-interpret": "^0.9.3-marketa.1",
|
||||
"axios": "^0.26.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"buffer": "^6.0.3",
|
||||
"bullmq": "^5.12.15",
|
||||
"cookie-parser": "^1.4.6",
|
||||
|
||||
5
server/src/api/index.ts
Normal file
5
server/src/api/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
const genAPIKey = () => {
|
||||
return [...Array(30)]
|
||||
.map((e) => ((Math.random() * 36) | 0).toString(36))
|
||||
.join('');
|
||||
};
|
||||
@@ -2,13 +2,10 @@ import { Sequelize } from 'sequelize';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const sequelize = new Sequelize(
|
||||
process.env.DB_NAME as string,
|
||||
process.env.DB_USER as string,
|
||||
process.env.DB_PASSWORD as string,
|
||||
`postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`,
|
||||
{
|
||||
host: process.env.DB_HOST,
|
||||
host: 'localhost',
|
||||
dialect: 'postgres',
|
||||
logging: false,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DataTypes, Model, Optional } from 'sequelize';
|
||||
import sequelize from '../db/config';
|
||||
import { hashPassword, comparePassword } from '../utils/auth';
|
||||
|
||||
interface UserAttributes {
|
||||
id: number;
|
||||
@@ -15,16 +14,12 @@ class User extends Model<UserAttributes, UserCreationAttributes> implements User
|
||||
public id!: number;
|
||||
public email!: string;
|
||||
public password!: string;
|
||||
|
||||
public async isValidPassword(password: string): Promise<boolean> {
|
||||
return comparePassword(password, this.password);
|
||||
}
|
||||
}
|
||||
|
||||
User.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
@@ -44,13 +39,6 @@ User.init(
|
||||
{
|
||||
sequelize,
|
||||
tableName: 'user',
|
||||
hooks: {
|
||||
beforeCreate: async (user: User) => {
|
||||
if (user.password) {
|
||||
user.password = await hashPassword(user.password) as string;
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import User from '../models/User';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { hashPassword, comparePassword } from '../utils/auth';
|
||||
import { requireSignIn } from '../middlewares/auth';
|
||||
export const router = Router();
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
@@ -17,7 +19,9 @@ router.post('/register', async (req, res) => {
|
||||
let userExist = await User.findOne({ where: { email } });
|
||||
if (userExist) return res.status(400).send('User already exists')
|
||||
|
||||
const user = await User.create({ email, password });
|
||||
const hashedPassword = await hashPassword(password)
|
||||
|
||||
const user = await User.create({ email, password: hashedPassword });
|
||||
|
||||
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET as string, { expiresIn: '1h' });
|
||||
user.password = undefined as unknown as string
|
||||
@@ -36,8 +40,10 @@ router.post('/login', async (req, res) => {
|
||||
if (!email || !password) return res.status(400).send('Email and password are required')
|
||||
if (password.length < 6) return res.status(400).send('Password must be at least 6 characters')
|
||||
|
||||
let user = await User.findOne({ where: { email } });
|
||||
const match = await user?.isValidPassword(password);
|
||||
let user = await User.findOne({ raw: true, where: { email } });
|
||||
if (!user) return res.status(400).send('User does not exist');
|
||||
|
||||
const match = await comparePassword(password, user.password)
|
||||
if (!match) return res.status(400).send('Invalid email or password')
|
||||
|
||||
const token = jwt.sign({ id: user?.id }, process.env.JWT_SECRET as string, { expiresIn: '1h' });
|
||||
@@ -52,6 +58,7 @@ router.post('/login', async (req, res) => {
|
||||
res.json(user)
|
||||
} catch (error: any) {
|
||||
res.status(400).send(`Could not login user - ${error.message}`)
|
||||
console.log(`Could not login user - ${error}`)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -64,16 +71,21 @@ router.get('/logout', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/current-user', async (req: AuthenticatedRequest, res) => {
|
||||
router.get('/current-user', requireSignIn, async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).send('Unauthorized');
|
||||
return res.status(401).json({ ok: false, error: 'Unauthorized' });
|
||||
}
|
||||
const user = await User.findByPk(req.user.id, {
|
||||
attributes: { exclude: ['password'] },
|
||||
});
|
||||
return res.status(200).json({ ok: true });
|
||||
if (!user) {
|
||||
return res.status(404).json({ ok: false, error: 'User not found' });
|
||||
} else {
|
||||
return res.status(200).json({ ok: true, user: user });
|
||||
}
|
||||
} catch (error: any) {
|
||||
return res.status(500).send(`Could not fetch current user : ${error.message}.`);
|
||||
console.error('Error in current-user route:', error);
|
||||
return res.status(500).json({ ok: false, error: `Could not fetch current user: ${error.message}` });
|
||||
}
|
||||
});
|
||||
@@ -1,11 +1,13 @@
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import cors from 'cors';
|
||||
import 'dotenv/config';
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
import { record, workflow, storage, auth, integration } from './routes';
|
||||
import { BrowserPool } from "./browser-management/classes/BrowserPool";
|
||||
import logger from './logger';
|
||||
import { connectDB, syncDB } from './db/config';
|
||||
import bodyParser from 'body-parser';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import csrf from 'csurf';
|
||||
import { SERVER_PORT } from "./constants/config";
|
||||
@@ -14,7 +16,10 @@ import { Server } from "socket.io";
|
||||
const csrfProtection = csrf({ cookie: true })
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(cors({
|
||||
origin: 'http://localhost:3000',
|
||||
credentials: true,
|
||||
}));
|
||||
app.use(express.json());
|
||||
|
||||
const server = http.createServer(app);
|
||||
@@ -30,6 +35,8 @@ export const io = new Server(server);
|
||||
*/
|
||||
export const browserPool = new BrowserPool();
|
||||
|
||||
app.use(bodyParser.json({ limit: '10mb' }))
|
||||
app.use(bodyParser.urlencoded({ extended: true, limit: '10mb', parameterLimit: 10000 }));
|
||||
// parse cookies - "cookie" is true in csrfProtection
|
||||
app.use(cookieParser())
|
||||
app.use(csrfProtection)
|
||||
@@ -41,12 +48,12 @@ app.use('/auth', auth);
|
||||
app.use('/integration', integration);
|
||||
|
||||
app.get('/', function (req, res) {
|
||||
return res.send('Maxun server started 🚀');
|
||||
return res.send('Maxun server started 🚀');
|
||||
});
|
||||
|
||||
app.get('/csrf-token', (req, res) => {
|
||||
res.json({ csrfToken: req.csrfToken() })
|
||||
})
|
||||
res.json({ csrfToken: req.csrfToken() })
|
||||
})
|
||||
|
||||
server.listen(SERVER_PORT, async () => {
|
||||
await connectDB();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
export const hashPassword = (password: any) => {
|
||||
export const hashPassword = (password: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
bcrypt.genSalt(12, (err, salt) => {
|
||||
if (err) {
|
||||
@@ -17,6 +17,6 @@ export const hashPassword = (password: any) => {
|
||||
}
|
||||
|
||||
// password from frontend and hash from database
|
||||
export const comparePassword = (password: any, hash: any) => {
|
||||
export const comparePassword = (password: string, hash: string): Promise<boolean> => {
|
||||
return bcrypt.compare(password, hash)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useContext } from 'react';
|
||||
import axios from 'axios';
|
||||
import styled from "styled-components";
|
||||
import { stopRecording } from "../../api/recording";
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
@@ -7,7 +8,8 @@ import { RecordingIcon } from "../atoms/RecorderIcon";
|
||||
import { SaveRecording } from "./SaveRecording";
|
||||
import { Circle } from "@mui/icons-material";
|
||||
import MeetingRoomIcon from '@mui/icons-material/MeetingRoom';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { AuthContext } from '../../context/auth';
|
||||
|
||||
interface NavBarProps {
|
||||
newRecording: () => void;
|
||||
@@ -18,8 +20,18 @@ interface NavBarProps {
|
||||
export const NavBar = ({ newRecording, recordingName, isRecording }: NavBarProps) => {
|
||||
|
||||
const { notify, browserId, setBrowserId, recordingLength } = useGlobalInfoStore();
|
||||
const { state, dispatch } = useContext(AuthContext);
|
||||
const { user } = state;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const logout = async () => {
|
||||
dispatch({ type: 'LOGOUT' });
|
||||
window.localStorage.removeItem('user');
|
||||
const { data } = await axios.get('http://localhost:8080/auth/logout');
|
||||
notify('success', data.message);
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
// If recording is in progress, the resources and change page view by setting browserId to null
|
||||
// else it won't affect the page
|
||||
const goToMainMenu = async () => {
|
||||
@@ -94,6 +106,23 @@ export const NavBar = ({ newRecording, recordingName, isRecording }: NavBarProps
|
||||
<MeetingRoomIcon sx={{ marginRight: '5px' }} />
|
||||
exit</Button>
|
||||
: null}
|
||||
{
|
||||
user !== null ? (
|
||||
<Button sx={{
|
||||
width: '100px',
|
||||
background: '#fff',
|
||||
color: 'rgba(25, 118, 210, 0.7)',
|
||||
padding: '9px',
|
||||
marginRight: '19px',
|
||||
'&:hover': {
|
||||
background: 'white',
|
||||
color: 'rgb(25, 118, 210)',
|
||||
}
|
||||
}} onClick={logout}>
|
||||
<MeetingRoomIcon sx={{ marginRight: '5px' }} />
|
||||
logout</Button>
|
||||
) : ""
|
||||
}
|
||||
</div>
|
||||
|
||||
</NavBarWrapper>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useReducer, createContext, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface ProviderProps {
|
||||
interface AuthProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const initialState = {
|
||||
user: null,
|
||||
};
|
||||
|
||||
const Context = createContext<{
|
||||
const AuthContext = createContext<{
|
||||
state: InitialStateType;
|
||||
dispatch: any;
|
||||
}>({
|
||||
@@ -44,10 +44,11 @@ const reducer = (state: InitialStateType, action: ActionType) => {
|
||||
}
|
||||
};
|
||||
|
||||
const Provider = ({ children }: ProviderProps) => {
|
||||
const AuthProvider = ({ children }: AuthProviderProps) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const navigate = useNavigate();
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
// get user info from local storage
|
||||
useEffect(() => {
|
||||
@@ -88,16 +89,24 @@ const Provider = ({ children }: ProviderProps) => {
|
||||
// csrf - include tokens in the axios header every time a request is made
|
||||
useEffect(() => {
|
||||
const getCsrfToken = async () => {
|
||||
const { data } = await axios.get('http://localhost:8080/csrf-token');
|
||||
console.log('CSRFFFFF =>>>>', data);
|
||||
(axios.defaults.headers as any)['X-CSRF-TOKEN'] = data.getCsrfToken;
|
||||
try {
|
||||
const { data } = await axios.get('http://localhost:8080/csrf-token');
|
||||
console.log('CSRF Token Response:', data);
|
||||
if (data && data.csrfToken) {
|
||||
(axios.defaults.headers as any)['X-CSRF-TOKEN'] = data.csrfToken;
|
||||
} else {
|
||||
console.error('CSRF token not found in the response');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching CSRF token:', error);
|
||||
}
|
||||
};
|
||||
getCsrfToken();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ state, dispatch }}>{children}</Context.Provider>
|
||||
<AuthContext.Provider value={{ state, dispatch }}>{children}</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { Context, Provider };
|
||||
export { AuthContext, AuthProvider };
|
||||
|
||||
119
src/pages/Login.tsx
Normal file
119
src/pages/Login.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import axios from 'axios';
|
||||
import { useState, useContext, useEffect } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { AuthContext } from '../context/auth';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
CircularProgress,
|
||||
} from '@mui/material';
|
||||
import { useGlobalInfoStore } from "../context/globalInfo";
|
||||
|
||||
const Login = () => {
|
||||
const [form, setForm] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { notify } = useGlobalInfoStore();
|
||||
const { email, password } = form;
|
||||
|
||||
const { state, dispatch } = useContext(AuthContext);
|
||||
const { user } = state;
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [user, navigate]);
|
||||
|
||||
const handleChange = (e: any) => {
|
||||
const { name, value } = e.target;
|
||||
setForm({ ...form, [name]: value });
|
||||
};
|
||||
|
||||
const submitForm = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await axios.post(`http://localhost:8080/auth/login`, { email, password });
|
||||
dispatch({ type: 'LOGIN', payload: data });
|
||||
notify('success', 'Welcome to Maxun!');
|
||||
window.localStorage.setItem('user', JSON.stringify(data));
|
||||
navigate('/');
|
||||
} catch (err: any) {
|
||||
notify('error', err.response.data || 'Login Failed. Please try again.');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
mt: 5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Welcome Back!
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={submitForm} sx={{ maxWidth: 400, width: '100%' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Email"
|
||||
name="email"
|
||||
value={email}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Password"
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: 2, mb: 2 }}
|
||||
disabled={loading || !email || !password}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<CircularProgress size={20} sx={{ mr: 2 }} />
|
||||
Loading
|
||||
</>
|
||||
) : (
|
||||
'Login'
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Redirect to Register */}
|
||||
<Typography variant="body2" align="center">
|
||||
Don’t have an account?{' '}
|
||||
<Link to="/register" style={{ textDecoration: 'none', color: '#1976d2' }}>
|
||||
Register
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -2,11 +2,15 @@ import React, { useEffect, useState } from 'react';
|
||||
import { NavBar } from "../components/molecules/NavBar";
|
||||
import { SocketProvider } from "../context/socket";
|
||||
import { BrowserDimensionsProvider } from "../context/browserDimensions";
|
||||
import { AuthProvider } from '../context/auth';
|
||||
import { RecordingPage } from "./RecordingPage";
|
||||
import { MainPage } from "./MainPage";
|
||||
import { useGlobalInfoStore } from "../context/globalInfo";
|
||||
import { getActiveBrowserId } from "../api/recording";
|
||||
import { AlertSnackbar } from "../components/atoms/AlertSnackbar";
|
||||
import Login from './Login';
|
||||
import Register from './Register';
|
||||
import UserRoute from '../routes/userRoute';
|
||||
import { Routes, Route, useNavigate } from 'react-router-dom';
|
||||
|
||||
export const PageWrapper = () => {
|
||||
@@ -50,25 +54,33 @@ export const PageWrapper = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SocketProvider>
|
||||
<React.Fragment>
|
||||
<NavBar newRecording={handleNewRecording} recordingName={recordingName} isRecording={!!browserId} />
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={<MainPage handleEditRecording={handleEditRecording} />}
|
||||
/>
|
||||
<Route
|
||||
path="/recording"
|
||||
element={
|
||||
<BrowserDimensionsProvider>
|
||||
<RecordingPage recordingName={recordingName} />
|
||||
</BrowserDimensionsProvider>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</React.Fragment>
|
||||
</SocketProvider>
|
||||
<AuthProvider>
|
||||
<SocketProvider>
|
||||
<React.Fragment>
|
||||
<NavBar newRecording={handleNewRecording} recordingName={recordingName} isRecording={!!browserId} />
|
||||
<Routes>
|
||||
<Route element={<UserRoute />}>
|
||||
<Route path="/" element={<MainPage handleEditRecording={handleEditRecording} />} />
|
||||
</Route>
|
||||
<Route element={<UserRoute />}>
|
||||
<Route path="/recording" element={
|
||||
<BrowserDimensionsProvider>
|
||||
<RecordingPage recordingName={recordingName} />
|
||||
</BrowserDimensionsProvider>
|
||||
} />
|
||||
</Route>
|
||||
<Route
|
||||
path="/login"
|
||||
element={<Login />}
|
||||
/>
|
||||
<Route
|
||||
path="/register"
|
||||
element={<Register />}
|
||||
/>
|
||||
</Routes>
|
||||
</React.Fragment>
|
||||
</SocketProvider>
|
||||
</AuthProvider>
|
||||
{isNotification() ?
|
||||
<AlertSnackbar severity={notification.severity}
|
||||
message={notification.message}
|
||||
|
||||
114
src/pages/Register.tsx
Normal file
114
src/pages/Register.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useState, useContext, useEffect } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { AuthContext } from '../context/auth';
|
||||
import { TextField, Button, CircularProgress, Typography, Box, Container } from '@mui/material';
|
||||
import { useGlobalInfoStore } from "../context/globalInfo";
|
||||
|
||||
const Register = () => {
|
||||
const [form, setForm] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { notify } = useGlobalInfoStore();
|
||||
const { email, password } = form;
|
||||
|
||||
const { state, dispatch } = useContext(AuthContext);
|
||||
const { user } = state;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (user !== null) navigate('/');
|
||||
}, [user, navigate]);
|
||||
|
||||
const handleChange = (e: any) => {
|
||||
const { name, value } = e.target;
|
||||
setForm({ ...form, [name]: value });
|
||||
};
|
||||
|
||||
const submitForm = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await axios.post('http://localhost:8080/auth/register', {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
dispatch({
|
||||
type: 'LOGIN',
|
||||
payload: data,
|
||||
});
|
||||
notify('success', 'Welcome to Maxun!');
|
||||
window.localStorage.setItem('user', JSON.stringify(data));
|
||||
navigate('/');
|
||||
} catch (err: any) {
|
||||
notify('error', err.response.data || 'Registration Failed. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm" sx={{ mt: 8 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography component="h1" variant="h5">
|
||||
Create an account
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={submitForm} sx={{ mt: 1 }}>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
id="email"
|
||||
label="Email Address"
|
||||
name="email"
|
||||
value={email}
|
||||
onChange={handleChange}
|
||||
autoComplete="email"
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={handleChange}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: 3, mb: 2 }}
|
||||
disabled={loading || !email || !password}
|
||||
>
|
||||
{loading ? (
|
||||
<CircularProgress size={24} sx={{ color: '#fff' }} />
|
||||
) : (
|
||||
'Register'
|
||||
)}
|
||||
</Button>
|
||||
<Typography variant="body2" align="center">
|
||||
Already have an account?{' '}
|
||||
<Link to="/login" style={{ textDecoration: 'none', color: '#1976d2' }}>
|
||||
Login
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
12
src/routes/userRoute.tsx
Normal file
12
src/routes/userRoute.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
import { useContext } from 'react';
|
||||
import { AuthContext } from '../context/auth';
|
||||
|
||||
const UserRoute = () => {
|
||||
const { state } = useContext(AuthContext);
|
||||
|
||||
return state.user ? <Outlet /> : <Navigate to="/login" />;
|
||||
};
|
||||
|
||||
export default UserRoute;
|
||||
Reference in New Issue
Block a user