Commit 2584765b authored by Egor Kremnev's avatar Egor Kremnev

add role authorization. fix add product form. refactor routes

parent ddb7b99c
...@@ -6,7 +6,7 @@ module.exports = { ...@@ -6,7 +6,7 @@ module.exports = {
port: 8001, port: 8001,
uploadPath: path.join(rootPath, 'public', 'uploads'), uploadPath: path.join(rootPath, 'public', 'uploads'),
db: { db: {
host: 'mongodb://localhost', host: 'mongodb://127.0.0.1',
database: 'shop', database: 'shop',
} }
}; };
...@@ -54,12 +54,14 @@ db.once('open', async () => { ...@@ -54,12 +54,14 @@ db.once('open', async () => {
{ {
username: "user", username: "user",
password: "qwerty", password: "qwerty",
token: null token: null,
role: 'user'
}, },
{ {
username: "admin", username: "admin",
password: "qwerty", password: "qwerty",
token: null token: null,
role: 'admin'
} }
]); ]);
......
const User = require("../models/User"); const User = require("../models/User");
const secureRoute = async (req, res, next) => { const auth = async (req, res, next) => {
const token = req.get('Authorization'); const token = req.get('Authorization');
if (!token) return res if (!token) return res
...@@ -13,7 +13,9 @@ const secureRoute = async (req, res, next) => { ...@@ -13,7 +13,9 @@ const secureRoute = async (req, res, next) => {
.status(401) .status(401)
.send('Token is wrong'); .send('Token is wrong');
req.user = user;
next(); next();
} }
module.exports = secureRoute; module.exports = auth;
const permit = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).send({message: 'Unauthenticated'});
}
if (!roles.includes(req.user.role)) {
return res.status(403).send({message: 'Unauthorized'});
}
next();
};
};
module.exports = permit;
...@@ -33,6 +33,12 @@ const UserSchema = new Schema({ ...@@ -33,6 +33,12 @@ const UserSchema = new Schema({
}, },
message: "Token duplicated" message: "Token duplicated"
} }
},
role: {
type: String,
required: true,
default: 'user',
enum: ['user', 'admin']
} }
}); });
......
const router = require('express').Router(); const router = require('express').Router();
const Category = require('../models/Category'); const Category = require('../models/Category');
const secureRoute = require("../middleware/secureRoute"); const auth = require("../middleware/auth");
const permit = require("../middleware/permit");
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
try { try {
...@@ -10,7 +11,7 @@ router.get('/', async (req, res) => { ...@@ -10,7 +11,7 @@ router.get('/', async (req, res) => {
} }
}); });
router.post('/', secureRoute, async (req, res) => { router.post('/', [auth, permit('admin')], async (req, res) => {
const category = new Category(req.body); const category = new Category(req.body);
try { try {
......
...@@ -5,7 +5,8 @@ const multer = require('multer'); ...@@ -5,7 +5,8 @@ const multer = require('multer');
const path = require('path'); const path = require('path');
const {uploadPath} = require('./../config'); const {uploadPath} = require('./../config');
const Product = require('../models/Product'); const Product = require('../models/Product');
const secureRoute = require("../middleware/secureRoute"); const auth = require("../middleware/auth");
const permit = require("../middleware/permit");
const storage = multer.diskStorage({ const storage = multer.diskStorage({
destination: (req, file, cb) => { destination: (req, file, cb) => {
...@@ -19,7 +20,7 @@ const storage = multer.diskStorage({ ...@@ -19,7 +20,7 @@ const storage = multer.diskStorage({
const upload = multer({storage}); const upload = multer({storage});
const createRoutes = () => { const createRoutes = () => {
router.post('/', upload.single('image'), secureRoute, async (req, res) => { router.post('/', [auth, permit('admin', 'manager'), upload.single('image')], async (req, res) => {
const productData = {...req.body}; const productData = {...req.body};
if (req.file) productData.image = req.file.filename; if (req.file) productData.image = req.file.filename;
......
const router = require('express').Router(); const router = require('express').Router();
const User = require('../models/User'); const User = require('../models/User');
const secureRoute = require("../middleware/secureRoute"); const auth = require("../middleware/auth");
router.post('/', async (req, res) => { router.post('/', async (req, res) => {
try { try {
const user = new User(req.body); const user = new User({
username: req.body.username,
password: req.body.password
});
user.generateToken(); user.generateToken();
await user.save(); await user.save();
...@@ -38,25 +41,18 @@ router.post('/login', async (req, res) => { ...@@ -38,25 +41,18 @@ router.post('/login', async (req, res) => {
} }
}); });
router.get('/profile', secureRoute, async (req, res) => { router.get('/profile', auth, async (req, res) => {
const token = req.get('Authorization');
const user = await User.findOne({token});
res.send({ res.send({
message: "Большой большой сикрет", message: "Большой большой сикрет",
username: user.username username: req.user.username
}); });
}); });
router.delete('/logout', secureRoute, async (req, res) => { router.delete('/logout', auth, async (req, res) => {
const token = req.get('Authorization'); req.user.token = null;
const success = {message: 'Success'}; req.user.save();
const user = await User.findOne({token});
user.token = null;
user.save();
res.send(success); res.send({message: 'Success'});
}); });
module.exports = router; module.exports = router;
import {Route, Routes} from "react-router-dom";
import {LOGIN, PRODUCT_ADD, PRODUCT_LIST, PRODUCT_VIEW, REGISTER} from "./constants/routes";
import Layout from "./components/Layout/Layout";
import Products from "./containers/Products/Products";
import AddProduct from "./containers/AddProduct/AddProduct";
import Register from "./containers/Auth/Register/Register";
import Login from "./containers/Auth/Login/Login";
import {useSelector} from "react-redux"; import {useSelector} from "react-redux";
import ProtectedRoute from "./components/ProtectedRoute/ProtectedRoute"; import Routes from "./Routes";
const App = () => { const App = () => {
const user = useSelector(({usersState}) => usersState.user); const user = useSelector(({usersState}) => usersState.user);
const productAdd = <ProtectedRoute return <Routes user={user} />;
isAllowed={!!user}
redirectPath={LOGIN}
>
<AddProduct />
</ProtectedRoute>;
return <Routes>
<Route element={<Layout />}>
<Route index element={<Products />}/>
<Route path={REGISTER} element={<Register />}/>
<Route path={LOGIN} element={<Login />}/>
<Route path={PRODUCT_LIST} element={<Products />}/>
<Route path={PRODUCT_ADD} element={productAdd}/>
<Route path={PRODUCT_VIEW} element={<h1>Show product</h1>}/>
</Route>
</Routes>;
}; };
......
import {Navigate, Outlet, Route, Routes as RoutesSwitch} from "react-router-dom";
import {LOGIN, MAIN, PRODUCT_ADD, PRODUCT_LIST, PRODUCT_VIEW, REGISTER} from "./constants/routes";
import AddProduct from "./containers/AddProduct/AddProduct";
import Layout from "./components/Layout/Layout";
import Products from "./containers/Products/Products";
import Register from "./containers/Auth/Register/Register";
import Login from "./containers/Auth/Login/Login";
const ProtectedRoute = ({isAllowed, redirectPath, children}) => {
if (!isAllowed) {
return <Navigate to={redirectPath} replace />;
}
return children || <Outlet/>;
};
const Routes = ({user}) => {
const productAdd = <ProtectedRoute
isAllowed={!!user && user.role === 'admin'}
redirectPath={!!user ? MAIN : LOGIN}
>
<AddProduct />
</ProtectedRoute>;
return <RoutesSwitch>
<Route element={<Layout />}>
<Route index element={<Products />}/>
<Route path={REGISTER} element={<Register />}/>
<Route path={LOGIN} element={<Login />}/>
<Route path={PRODUCT_LIST} element={<Products />}/>
<Route path={PRODUCT_ADD} element={productAdd}/>
<Route path={PRODUCT_VIEW} element={<h1>Show product</h1>}/>
</Route>
</RoutesSwitch>;
};
export default Routes;
...@@ -5,14 +5,4 @@ const instance = axios.create({ ...@@ -5,14 +5,4 @@ const instance = axios.create({
baseURL: apiUrl + "/api/v1" baseURL: apiUrl + "/api/v1"
}); });
instance.interceptors.request.use(function (config) {
if (localStorage.getItem('user') !== null) {
config.headers.Authorization = JSON.parse(localStorage.getItem('user')).token;
}
return config;
}, function(error) {
return Promise.reject(error);
});
export default instance; export default instance;
import {useState} from "react"; import {useState} from "react";
import {Button, Grid, TextField} from "@mui/material"; import {Button, Grid} from "@mui/material";
import FileInput from "../../UI/Form/FileInput/FileInput"; import FileInput from "../../UI/Form/FileInput/FileInput";
import FormElement from "../../UI/Form/FormElement/FormElement";
const ProductForm = ({createProductHandler}) => { const ProductForm = ({createProductHandler, categories}) => {
const [state, setState] = useState({ const [state, setState] = useState({
title: "", title: "",
price: "", price: "",
description: "", description: "",
image: '' category: "",
image: ""
}); });
const submitFormHandler = e => { const submitFormHandler = e => {
...@@ -46,48 +48,43 @@ const ProductForm = ({createProductHandler}) => { ...@@ -46,48 +48,43 @@ const ProductForm = ({createProductHandler}) => {
onSubmit={submitFormHandler} onSubmit={submitFormHandler}
> >
<Grid container direction="column" spacing={2}> <Grid container direction="column" spacing={2}>
<Grid item xs> <FormElement
<TextField id="title"
fullWidth label="Title"
variant="outlined" value={state.title}
id="title" onChange={inputChangeHandler}
label="Title" name="title"
value={state.title} />
onChange={inputChangeHandler} <FormElement
name="title" id="price"
/> label="Price"
</Grid> value={state.price}
<Grid item xs> onChange={inputChangeHandler}
<TextField name="price"
fullWidth />
variant="outlined" <FormElement
id="price" multiline={true}
label="Price" rows={3}
value={state.price} id="description"
onChange={inputChangeHandler} label="Description"
name="price" value={state.description}
/> onChange={inputChangeHandler}
</Grid> name="description"
<Grid item xs> />
<TextField <FormElement
fullWidth id="category"
multiline label="Category"
rows={3} value={state.category}
variant="outlined" onChange={inputChangeHandler}
id="description" name="category"
label="Description" select={true}
value={state.description} options={categories}
onChange={inputChangeHandler} />
name="description" <FileInput
/> onChange={onFileChangeHandler}
</Grid> name="image"
<Grid item xs> label="Image"
<FileInput />
onChange={onFileChangeHandler}
name="image"
label="Image"
/>
</Grid>
<Grid item xs> <Grid item xs>
<Button type="submit" color="primary" variant="contained">Create</Button> <Button type="submit" color="primary" variant="contained">Create</Button>
</Grid> </Grid>
......
import {Navigate, Outlet} from "react-router-dom";
const ProtectedRoute = ({isAllowed, redirectPath, children}) => {
if (!isAllowed) {
return <Navigate to={redirectPath} replace />;
}
return children || <Outlet/>;
};
export default ProtectedRoute;
import {Grid, TextField} from "@mui/material"; import {Grid, MenuItem, TextField} from "@mui/material";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
const FomElement = ({name, label, value, onChange, required, error, type}) => { const FormElement = ({
name,
label,
value,
onChange,
required,
error,
type,
select,
multiline,
rows,
options
}) => {
let inputChildren = null;
if (select) {
inputChildren = options.map(option => (
<MenuItem key={option._id} value={option._id}>
{option.title}
</MenuItem>
));
}
return <Grid item xs={12}> return <Grid item xs={12}>
<TextField <TextField
fullWidth fullWidth
...@@ -15,18 +37,27 @@ const FomElement = ({name, label, value, onChange, required, error, type}) => { ...@@ -15,18 +37,27 @@ const FomElement = ({name, label, value, onChange, required, error, type}) => {
onChange={onChange} onChange={onChange}
autoComplete={name} autoComplete={name}
type={type} type={type}
/> multiline={multiline}
rows={rows}
select={select}
>
{inputChildren}
</TextField>
</Grid>; </Grid>;
}; };
FomElement.propTypes = { FormElement.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
type: PropTypes.string, type: PropTypes.string,
error: PropTypes.string, error: PropTypes.string,
required: PropTypes.bool, required: PropTypes.bool,
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired,
select: PropTypes.bool,
multiline: PropTypes.bool,
rows: PropTypes.number,
options: PropTypes.arrayOf(PropTypes.object)
}; };
export default FomElement; export default FormElement;
import ProductForm from "../../components/Product/ProductForm/ProductForm"; import ProductForm from "../../components/Product/ProductForm/ProductForm";
import {Typography} from "@mui/material"; import {Typography} from "@mui/material";
import {createProduct} from "../../store/actions/productsActions"; import {createProduct} from "../../store/actions/productsActions";
import {useDispatch} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {fetchCategories} from "../../store/actions/categoriesActions";
import {useEffect} from "react";
const AddProduct = () => { const AddProduct = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const categories = useSelector(({categoriesState}) => categoriesState.categories);
useEffect(() => {
dispatch(fetchCategories());
}, []);
const onProductFormSubmit = async data => { const onProductFormSubmit = async data => {
await dispatch(createProduct({data, callback: () => navigate('/')})); await dispatch(createProduct({data, callback: () => navigate('/')}));
...@@ -18,7 +25,10 @@ const AddProduct = () => { ...@@ -18,7 +25,10 @@ const AddProduct = () => {
<Typography variant='h4'> <Typography variant='h4'>
New Product New Product
</Typography> </Typography>
<ProductForm createProductHandler={onProductFormSubmit}/> <ProductForm
createProductHandler={onProductFormSubmit}
categories={categories}
/>
</> </>
); );
}; };
......
...@@ -6,7 +6,7 @@ import {Link as RouterLink, useLocation, useNavigate} from 'react-router-dom'; ...@@ -6,7 +6,7 @@ import {Link as RouterLink, useLocation, useNavigate} from 'react-router-dom';
import {REGISTER} from "../../../constants/routes"; import {REGISTER} from "../../../constants/routes";
import {useDispatch, useSelector} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import {loginUser} from "../../../store/actions/usersActions"; import {loginUser} from "../../../store/actions/usersActions";
import FomElement from "../../../components/UI/Form/FormElement/FomElement"; import FormElement from "../../../components/UI/Form/FormElement/FormElement";
import {setLoginError} from "../../../store/services/usersSlice"; import {setLoginError} from "../../../store/services/usersSlice";
const theme = createTheme(); const theme = createTheme();
...@@ -63,14 +63,14 @@ const Login = () => { ...@@ -63,14 +63,14 @@ const Login = () => {
{error && <Alert severity="error">{error.error}</Alert>} {error && <Alert severity="error">{error.error}</Alert>}
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}> <Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
<Grid container spacing={2}> <Grid container spacing={2}>
<FomElement <FormElement
required={true} required={true}
label="Username" label="Username"
name="username" name="username"
onChange={inputChangeHandler} onChange={inputChangeHandler}
value={state.username} value={state.username}
/> />
<FomElement <FormElement
required={true} required={true}
name="password" name="password"
label="Password" label="Password"
......
...@@ -6,7 +6,7 @@ import {Link as RouterLink, useLocation, useNavigate} from 'react-router-dom'; ...@@ -6,7 +6,7 @@ import {Link as RouterLink, useLocation, useNavigate} from 'react-router-dom';
import {LOGIN} from "../../../constants/routes"; import {LOGIN} from "../../../constants/routes";
import {useDispatch, useSelector} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import {registerUser} from "../../../store/actions/usersActions"; import {registerUser} from "../../../store/actions/usersActions";
import FomElement from "../../../components/UI/Form/FormElement/FomElement"; import FormElement from "../../../components/UI/Form/FormElement/FormElement";
import {setRegisterError} from "../../../store/services/usersSlice"; import {setRegisterError} from "../../../store/services/usersSlice";
const theme = createTheme(); const theme = createTheme();
...@@ -66,7 +66,7 @@ const Register = () => { ...@@ -66,7 +66,7 @@ const Register = () => {
</Typography> </Typography>
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}> <Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
<Grid container spacing={2}> <Grid container spacing={2}>
<FomElement <FormElement
required={true} required={true}
label="Username" label="Username"
name="username" name="username"
...@@ -74,7 +74,7 @@ const Register = () => { ...@@ -74,7 +74,7 @@ const Register = () => {
value={state.username} value={state.username}
error={getFieldError('username')} error={getFieldError('username')}
/> />
<FomElement <FormElement
required={true} required={true}
name="password" name="password"
label="Password" label="Password"
......
...@@ -9,6 +9,7 @@ import ProductList from "../../components/Product/ProductList/ProductList"; ...@@ -9,6 +9,7 @@ import ProductList from "../../components/Product/ProductList/ProductList";
const Products = () => { const Products = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const products = useSelector(({productsState}) => productsState.products); const products = useSelector(({productsState}) => productsState.products);
const user = useSelector(({usersState}) => usersState.user);
useEffect(() => { useEffect(() => {
dispatch(fetchProducts()); dispatch(fetchProducts());
...@@ -28,11 +29,13 @@ const Products = () => { ...@@ -28,11 +29,13 @@ const Products = () => {
Products list Products list
</Typography> </Typography>
</Grid> </Grid>
<Grid item> {
<Button color="primary" component={Link} to={PRODUCT_ADD}> user && user.role === 'admin' && <Grid item>
Add product <Button color="primary" component={Link} to={PRODUCT_ADD}>
</Button> Add product
</Grid> </Button>
</Grid>
}
</Grid> </Grid>
<ProductList products={products} /> <ProductList products={products} />
</Grid> </Grid>
......
...@@ -4,43 +4,9 @@ import './index.css'; ...@@ -4,43 +4,9 @@ import './index.css';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import {Provider} from "react-redux"; import {Provider} from "react-redux";
import {configureStore} from "@reduxjs/toolkit";
import productsReducer from './store/services/productsSlice';
import usersReducer from './store/services/usersSlice';
import {BrowserRouter} from "react-router-dom"; import {BrowserRouter} from "react-router-dom";
import store from './store/configureStore';
const localStorageMiddleware = ({getState}) => next => action => { import setup from "./services/setupInterceptors";
const result = next(action);
if (getState().usersState.user) {
localStorage.setItem('user', JSON.stringify(getState().usersState.user));
} else {
localStorage.removeItem('user');
}
return result;
};
const reHydrateStore = () => {
const userLocalStorage = localStorage.getItem('user');
if (userLocalStorage !== null || userLocalStorage !== 'null' || userLocalStorage !== '') {
return {
usersState: {
user: JSON.parse(localStorage.getItem('user'))
}
};
}
return undefined;
};
const store = configureStore({
reducer: {
usersState: usersReducer,
productsState: productsReducer
},
preloadedState: reHydrateStore(),
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(localStorageMiddleware)
});
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( root.render(
...@@ -51,6 +17,8 @@ root.render( ...@@ -51,6 +17,8 @@ root.render(
</Provider> </Provider>
); );
setup(store);
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log)) // to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
......
import axiosApi from "../api/axiosApi";
const setup = ({getState}) => {
axiosApi.interceptors.request.use(
config => {
const user = getState().usersState.user;
if (user) {
config.headers.Authorization = user.token;
}
return config;
},
error => Promise.reject(error)
);
};
export default setup;
import axios from "../../api/axiosApi";
import {createAsyncThunk} from "@reduxjs/toolkit";
export const fetchCategories = createAsyncThunk(
'categories/fetch',
async () => await axios.get('/categories').then(res => res.data)
);
...@@ -31,13 +31,14 @@ export const loginUser = createAsyncThunk( ...@@ -31,13 +31,14 @@ export const loginUser = createAsyncThunk(
export const logoutUser = createAsyncThunk( export const logoutUser = createAsyncThunk(
'users/logout', 'users/logout',
async (payload, {dispatch}) => await axiosApi async ({callback}, {dispatch}) => await axiosApi
.delete('/users/logout') .delete('/users/logout')
.then(res => { .then(res => {
dispatch(setUser(null)); dispatch(setUser(null));
payload.callback(); callback();
}) })
.catch(e => { .catch(e => {
dispatch(setUser(null));
if (e?.response?.data) dispatch(setLogoutError(e.response.data)); if (e?.response?.data) dispatch(setLogoutError(e.response.data));
else dispatch(setLogoutError(e)); else dispatch(setLogoutError(e));
throw e; throw e;
......
import {configureStore} from "@reduxjs/toolkit";
import usersReducer from "./services/usersSlice";
import productsReducer from "./services/productsSlice";
import categoriesReducer from "./services/categoriesSlice";
const localStorageMiddleware = ({getState}) => next => action => {
const result = next(action);
if (getState().usersState.user) {
localStorage.setItem('user', JSON.stringify(getState().usersState.user));
} else {
localStorage.removeItem('user');
}
return result;
};
const reHydrateStore = () => {
const userLocalStorage = localStorage.getItem('user');
if (userLocalStorage !== null || userLocalStorage !== 'null' || userLocalStorage !== '') {
return {
usersState: {
user: JSON.parse(localStorage.getItem('user'))
}
};
}
return undefined;
};
const store = configureStore({
reducer: {
usersState: usersReducer,
productsState: productsReducer,
categoriesState: categoriesReducer,
},
preloadedState: reHydrateStore(),
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(localStorageMiddleware)
});
export default store;
import {createSlice} from "@reduxjs/toolkit";
import {fetchCategories} from "../actions/categoriesActions";
const initialState = {
categories: []
};
const categoriesSlice = createSlice(({
name: 'categories',
initialState,
extraReducers: builder => {
builder
.addCase(
fetchCategories.fulfilled,
(state, {payload}) => {
state.categories = payload || [];
}
);
}
}));
export default categoriesSlice.reducer;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment