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 = {
port: 8001,
uploadPath: path.join(rootPath, 'public', 'uploads'),
db: {
host: 'mongodb://localhost',
host: 'mongodb://127.0.0.1',
database: 'shop',
}
};
......@@ -54,12 +54,14 @@ db.once('open', async () => {
{
username: "user",
password: "qwerty",
token: null
token: null,
role: 'user'
},
{
username: "admin",
password: "qwerty",
token: null
token: null,
role: 'admin'
}
]);
......
const User = require("../models/User");
const secureRoute = async (req, res, next) => {
const auth = async (req, res, next) => {
const token = req.get('Authorization');
if (!token) return res
......@@ -13,7 +13,9 @@ const secureRoute = async (req, res, next) => {
.status(401)
.send('Token is wrong');
req.user = user;
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({
},
message: "Token duplicated"
}
},
role: {
type: String,
required: true,
default: 'user',
enum: ['user', 'admin']
}
});
......
const router = require('express').Router();
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) => {
try {
......@@ -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);
try {
......
......@@ -5,7 +5,8 @@ const multer = require('multer');
const path = require('path');
const {uploadPath} = require('./../config');
const Product = require('../models/Product');
const secureRoute = require("../middleware/secureRoute");
const auth = require("../middleware/auth");
const permit = require("../middleware/permit");
const storage = multer.diskStorage({
destination: (req, file, cb) => {
......@@ -19,7 +20,7 @@ const storage = multer.diskStorage({
const upload = multer({storage});
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};
if (req.file) productData.image = req.file.filename;
......
const router = require('express').Router();
const User = require('../models/User');
const secureRoute = require("../middleware/secureRoute");
const auth = require("../middleware/auth");
router.post('/', async (req, res) => {
try {
const user = new User(req.body);
const user = new User({
username: req.body.username,
password: req.body.password
});
user.generateToken();
await user.save();
......@@ -38,25 +41,18 @@ router.post('/login', async (req, res) => {
}
});
router.get('/profile', secureRoute, async (req, res) => {
const token = req.get('Authorization');
const user = await User.findOne({token});
router.get('/profile', auth, async (req, res) => {
res.send({
message: "Большой большой сикрет",
username: user.username
username: req.user.username
});
});
router.delete('/logout', secureRoute, async (req, res) => {
const token = req.get('Authorization');
const success = {message: 'Success'};
const user = await User.findOne({token});
user.token = null;
user.save();
router.delete('/logout', auth, async (req, res) => {
req.user.token = null;
req.user.save();
res.send(success);
res.send({message: 'Success'});
});
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 ProtectedRoute from "./components/ProtectedRoute/ProtectedRoute";
import Routes from "./Routes";
const App = () => {
const user = useSelector(({usersState}) => usersState.user);
const productAdd = <ProtectedRoute
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>;
return <Routes user={user} />;
};
......
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({
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;
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 FormElement from "../../UI/Form/FormElement/FormElement";
const ProductForm = ({createProductHandler}) => {
const ProductForm = ({createProductHandler, categories}) => {
const [state, setState] = useState({
title: "",
price: "",
description: "",
image: ''
category: "",
image: ""
});
const submitFormHandler = e => {
......@@ -46,48 +48,43 @@ const ProductForm = ({createProductHandler}) => {
onSubmit={submitFormHandler}
>
<Grid container direction="column" spacing={2}>
<Grid item xs>
<TextField
fullWidth
variant="outlined"
<FormElement
id="title"
label="Title"
value={state.title}
onChange={inputChangeHandler}
name="title"
/>
</Grid>
<Grid item xs>
<TextField
fullWidth
variant="outlined"
<FormElement
id="price"
label="Price"
value={state.price}
onChange={inputChangeHandler}
name="price"
/>
</Grid>
<Grid item xs>
<TextField
fullWidth
multiline
<FormElement
multiline={true}
rows={3}
variant="outlined"
id="description"
label="Description"
value={state.description}
onChange={inputChangeHandler}
name="description"
/>
</Grid>
<Grid item xs>
<FormElement
id="category"
label="Category"
value={state.category}
onChange={inputChangeHandler}
name="category"
select={true}
options={categories}
/>
<FileInput
onChange={onFileChangeHandler}
name="image"
label="Image"
/>
</Grid>
<Grid item xs>
<Button type="submit" color="primary" variant="contained">Create</Button>
</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";
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}>
<TextField
fullWidth
......@@ -15,18 +37,27 @@ const FomElement = ({name, label, value, onChange, required, error, type}) => {
onChange={onChange}
autoComplete={name}
type={type}
/>
multiline={multiline}
rows={rows}
select={select}
>
{inputChildren}
</TextField>
</Grid>;
};
FomElement.propTypes = {
FormElement.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
type: PropTypes.string,
error: PropTypes.string,
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 {Typography} from "@mui/material";
import {createProduct} from "../../store/actions/productsActions";
import {useDispatch} from "react-redux";
import {useDispatch, useSelector} from "react-redux";
import {useNavigate} from "react-router-dom";
import {fetchCategories} from "../../store/actions/categoriesActions";
import {useEffect} from "react";
const AddProduct = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const categories = useSelector(({categoriesState}) => categoriesState.categories);
useEffect(() => {
dispatch(fetchCategories());
}, []);
const onProductFormSubmit = async data => {
await dispatch(createProduct({data, callback: () => navigate('/')}));
......@@ -18,7 +25,10 @@ const AddProduct = () => {
<Typography variant='h4'>
New Product
</Typography>
<ProductForm createProductHandler={onProductFormSubmit}/>
<ProductForm
createProductHandler={onProductFormSubmit}
categories={categories}
/>
</>
);
};
......
......@@ -6,7 +6,7 @@ import {Link as RouterLink, useLocation, useNavigate} from 'react-router-dom';
import {REGISTER} from "../../../constants/routes";
import {useDispatch, useSelector} from "react-redux";
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";
const theme = createTheme();
......@@ -63,14 +63,14 @@ const Login = () => {
{error && <Alert severity="error">{error.error}</Alert>}
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
<Grid container spacing={2}>
<FomElement
<FormElement
required={true}
label="Username"
name="username"
onChange={inputChangeHandler}
value={state.username}
/>
<FomElement
<FormElement
required={true}
name="password"
label="Password"
......
......@@ -6,7 +6,7 @@ import {Link as RouterLink, useLocation, useNavigate} from 'react-router-dom';
import {LOGIN} from "../../../constants/routes";
import {useDispatch, useSelector} from "react-redux";
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";
const theme = createTheme();
......@@ -66,7 +66,7 @@ const Register = () => {
</Typography>
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
<Grid container spacing={2}>
<FomElement
<FormElement
required={true}
label="Username"
name="username"
......@@ -74,7 +74,7 @@ const Register = () => {
value={state.username}
error={getFieldError('username')}
/>
<FomElement
<FormElement
required={true}
name="password"
label="Password"
......
......@@ -9,6 +9,7 @@ import ProductList from "../../components/Product/ProductList/ProductList";
const Products = () => {
const dispatch = useDispatch();
const products = useSelector(({productsState}) => productsState.products);
const user = useSelector(({usersState}) => usersState.user);
useEffect(() => {
dispatch(fetchProducts());
......@@ -28,11 +29,13 @@ const Products = () => {
Products list
</Typography>
</Grid>
<Grid item>
{
user && user.role === 'admin' && <Grid item>
<Button color="primary" component={Link} to={PRODUCT_ADD}>
Add product
</Button>
</Grid>
}
</Grid>
<ProductList products={products} />
</Grid>
......
......@@ -4,43 +4,9 @@ import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
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";
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
},
preloadedState: reHydrateStore(),
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(localStorageMiddleware)
});
import store from './store/configureStore';
import setup from "./services/setupInterceptors";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
......@@ -51,6 +17,8 @@ root.render(
</Provider>
);
setup(store);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// 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(
export const logoutUser = createAsyncThunk(
'users/logout',
async (payload, {dispatch}) => await axiosApi
async ({callback}, {dispatch}) => await axiosApi
.delete('/users/logout')
.then(res => {
dispatch(setUser(null));
payload.callback();
callback();
})
.catch(e => {
dispatch(setUser(null));
if (e?.response?.data) dispatch(setLogoutError(e.response.data));
else dispatch(setLogoutError(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