├── backend ├── .gitignore ├── images │ ├── default.png │ ├── profileimage59cb2d17-ca11-4598-9e89-090abf3f0908_data.png │ ├── profileimage5db8923c-fbc1-4d2f-ad25-d02f2de9781d_data.png │ └── profileimage7ce046e7-f818-45bf-931c-a84a38d7e781_data.png ├── .prettierrc.json ├── config │ ├── allowedOrigins.js │ ├── dbConn.js │ └── corsConfigs.js ├── routes │ ├── root.js │ ├── authRoutes.js │ ├── 404.js │ ├── noteRoutes.js │ ├── notificationRoutes.js │ └── userRoutes.js ├── .eslintrc.json ├── middleware │ ├── credentials.js │ ├── errorHandler.js │ ├── verifyJWT.js │ ├── loginLimiter.js │ └── logger.js ├── views │ ├── index.html │ └── 404.html ├── models │ ├── user.js │ ├── notification.js │ └── note.js ├── socketio.js ├── package.json ├── server.js └── controllers │ ├── noteController.js │ ├── authController.js │ ├── notificationController.js │ └── userController.js ├── frontend ├── .prettierignore ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── img │ │ └── data.png │ ├── manifest.json │ └── index.html ├── src │ ├── components │ │ ├── Layout.js │ │ ├── DashLayoutFooter.js │ │ ├── DashLayoutHeader.module.css │ │ ├── ProtectedRoutes.js │ │ ├── DashLayout.js │ │ ├── users │ │ │ ├── NewUserFrom.module.css │ │ │ ├── UsersList.js │ │ │ ├── Profile.js │ │ │ ├── NewUserFrom.js │ │ │ └── EditUserForm.js │ │ ├── PageNotFound.module.css │ │ ├── PageNotFound.js │ │ ├── Home.js │ │ ├── auth │ │ │ ├── PersistLogin.js │ │ │ ├── Welcome.js │ │ │ └── Login.js │ │ ├── DashLayoutHeader.js │ │ ├── notes │ │ │ ├── NotesList.js │ │ │ ├── NewNoteForm.js │ │ │ └── EditNoteForm.js │ │ └── notifications │ │ │ └── NotificationsList.js │ ├── config │ │ └── roles.js │ ├── api │ │ └── axios.js │ ├── hooks │ │ ├── useSocketIo.js │ │ ├── useRefreshAccessToken.js │ │ ├── useLocalStorage.js │ │ ├── useAuth.js │ │ └── useAxiosPrivate.js │ ├── recoil │ │ └── atom.js │ ├── index.js │ ├── App.js │ └── App.module.css ├── .gitignore ├── .prettierrc.json ├── .eslintrc.json └── package.json ├── .github └── FUNDING.yml ├── .vscode └── settings.json ├── SECURITY.md ├── LICENSE └── README.md /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | logs 3 | .env 4 | 5 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | /public/** 2 | /ressources/** 3 | /node_modules/** -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /backend/images/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelpro/MERN-auth-roles-boilerplate/HEAD/backend/images/default.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelpro/MERN-auth-roles-boilerplate/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/img/data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelpro/MERN-auth-roles-boilerplate/HEAD/frontend/public/img/data.png -------------------------------------------------------------------------------- /backend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | 3 | export default function Layout() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | ko_fi: adelbenyahia 3 | custom: ["https://www.paypal.me/adelbenyahia","https://www.buymeacoffee.com/adelbenyahia"] 4 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | 7 | # production 8 | /build 9 | /image 10 | # misc 11 | .vscode 12 | .env 13 | 14 | -------------------------------------------------------------------------------- /frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "jsxBracketSameLine": true 8 | } 9 | -------------------------------------------------------------------------------- /backend/images/profileimage59cb2d17-ca11-4598-9e89-090abf3f0908_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelpro/MERN-auth-roles-boilerplate/HEAD/backend/images/profileimage59cb2d17-ca11-4598-9e89-090abf3f0908_data.png -------------------------------------------------------------------------------- /backend/images/profileimage5db8923c-fbc1-4d2f-ad25-d02f2de9781d_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelpro/MERN-auth-roles-boilerplate/HEAD/backend/images/profileimage5db8923c-fbc1-4d2f-ad25-d02f2de9781d_data.png -------------------------------------------------------------------------------- /backend/images/profileimage7ce046e7-f818-45bf-931c-a84a38d7e781_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelpro/MERN-auth-roles-boilerplate/HEAD/backend/images/profileimage7ce046e7-f818-45bf-931c-a84a38d7e781_data.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "MERN DAV", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/config/roles.js: -------------------------------------------------------------------------------- 1 | const ROLES = [ 2 | { label: 'Employee', value: 'Employee' }, 3 | { label: 'Manager', value: 'Manager' }, 4 | { label: 'Admin', value: 'Admin' } 5 | ]; 6 | 7 | export default ROLES; 8 | -------------------------------------------------------------------------------- /backend/config/allowedOrigins.js: -------------------------------------------------------------------------------- 1 | const allowedOrigins = [ 2 | 'http://localhost:3000', 3 | 'http://127.0.0.1:3000', 4 | 'http://188.166.150.76:3000', 5 | 'https://mern-auth-roles.onrender.com', 6 | ] 7 | module.exports = allowedOrigins 8 | -------------------------------------------------------------------------------- /frontend/src/components/DashLayoutFooter.js: -------------------------------------------------------------------------------- 1 | import styles from '../App.module.css'; 2 | 3 | export default function DashLayoutFooter() { 4 | return ( 5 |
6 |

All right reserved - 2022

7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /backend/routes/root.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const path = require('path') 3 | const router = express.Router() 4 | router.get('^/%|/index(.html)?', (req, res) => { 5 | res.sendFile(path.join(__dirname, '..', 'views', 'index.html')) 6 | }) 7 | module.exports = router 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "addnote", 4 | "Autorenew", 5 | "dashstyles", 6 | "disabledevtools", 7 | "fvilers", 8 | "hookform", 9 | "MERN", 10 | "Recoilize", 11 | "signin", 12 | "Singup", 13 | "uiball" 14 | ], 15 | "cSpell.ignoreWords": ["yupref"] 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/components/DashLayoutHeader.module.css: -------------------------------------------------------------------------------- 1 | .dash__header__container { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | align-items: center; 6 | } 7 | 8 | @media only screen and (max-width: 800px) { 9 | .dash__header__container { 10 | flex-direction: column; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/api/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const baseURL = process.env.REACT_APP_BASEURL || 'http://localhost:3500'; 4 | export default axios.create({ 5 | baseURL 6 | }); 7 | export const axiosPrivate = axios.create({ 8 | baseURL, 9 | headers: { 'Content-Type': 'application/json' }, 10 | withCredentials: true 11 | }); 12 | -------------------------------------------------------------------------------- /backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "ecmaVersion": 12, 5 | "sourceType": "module" 6 | }, 7 | "extends": ["eslint:recommended", "prettier"], 8 | "env": { 9 | "es2021": true, 10 | "node": true 11 | }, 12 | "rules": { 13 | // "no-console": "error" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/middleware/credentials.js: -------------------------------------------------------------------------------- 1 | const allowedOrigins = require('../config/allowedOrigins') 2 | 3 | const credentials = (req, res, next) => { 4 | const origin = req.headers.origin 5 | if (allowedOrigins.includes(origin)) { 6 | res.header('Access-Control-Allow-Credentials', true) 7 | } 8 | next() 9 | } 10 | 11 | module.exports = credentials 12 | -------------------------------------------------------------------------------- /backend/config/dbConn.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const mongoDB_url = process.env.MONGODB_ATLAS_URL 4 | 5 | const connectDB = async () => { 6 | try { 7 | mongoose.connect(mongoDB_url, { 8 | useNewUrlParser: true, 9 | useUnifiedTopology: true, 10 | }) 11 | } catch (err) { 12 | console.log(err) 13 | } 14 | } 15 | module.exports = connectDB 16 | -------------------------------------------------------------------------------- /backend/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Backend API home 8 | 9 | 10 |

Backend API home

11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/src/hooks/useSocketIo.js: -------------------------------------------------------------------------------- 1 | import { io } from 'socket.io-client'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | const useSocketIo = () => { 5 | const [socket, setSocket] = useState(null); 6 | useEffect(() => { 7 | const connect = io.connect(process.env.REACT_APP_BASEURL); 8 | setSocket(connect); 9 | }, []); 10 | return { socket }; 11 | }; 12 | 13 | export default useSocketIo; 14 | -------------------------------------------------------------------------------- /backend/views/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 404 8 | 9 | 10 |

404

11 |

Page not found

12 | 13 | 14 | -------------------------------------------------------------------------------- /backend/routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const authController = require('../controllers/authController') 4 | const loginLimiter = require('../middleware/loginLimiter') 5 | 6 | router.route('/').post(loginLimiter, authController.login) 7 | router.route('/refresh').get(authController.refresh) 8 | router.route('/logout').post(authController.logout) 9 | 10 | module.exports = router 11 | -------------------------------------------------------------------------------- /backend/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const userSchema = new mongoose.Schema({ 4 | username: { type: String, required: true }, 5 | password: { type: String, required: true }, 6 | email: { type: String, required: true }, 7 | roles: [{ type: String, default: 'Employee' }], 8 | active: { type: Boolean, default: true }, 9 | profileImage: { 10 | type: String, 11 | }, 12 | }) 13 | module.exports = mongoose.model('User', userSchema) 14 | -------------------------------------------------------------------------------- /backend/middleware/errorHandler.js: -------------------------------------------------------------------------------- 1 | const { logEvents } = require('./logger') 2 | const errorHandler = (err, req, res) => { 3 | const message = `${err.name}\t${err.message}\t${req.method}\t${req.url}\t${req.headers?.origin}` 4 | logEvents(message, 'errLog.log') 5 | console.log(err.stack) 6 | const status = res.statusCode ? res.statusCode : 500 //serveur error code 7 | res.status(status) 8 | res.json({ message: err.message }) 9 | } 10 | module.exports = errorHandler 11 | -------------------------------------------------------------------------------- /frontend/src/components/ProtectedRoutes.js: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet, useLocation } from 'react-router-dom'; 2 | import useAuth from '../hooks/useAuth'; 3 | 4 | export default function ProtectedRoutes({ allowedRoles }) { 5 | const { roles } = useAuth(); 6 | // TODO error roles =[] null 7 | const location = useLocation(); 8 | return roles.some((role) => allowedRoles?.includes(role)) ? ( 9 | 10 | ) : ( 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/hooks/useRefreshAccessToken.js: -------------------------------------------------------------------------------- 1 | import axios from '../api/axios'; 2 | 3 | const useRefreshAccessToken = () => { 4 | const getNewToken = async () => { 5 | let refreshToken = null; 6 | await axios 7 | .get('/auth/refresh', { 8 | withCredentials: true 9 | }) 10 | .then((response) => { 11 | refreshToken = response.data.accessToken; 12 | }); 13 | return refreshToken; 14 | }; 15 | return getNewToken; 16 | }; 17 | export default useRefreshAccessToken; 18 | -------------------------------------------------------------------------------- /backend/routes/404.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const path = require('path') 4 | 5 | router.get('*', (req, res) => { 6 | res.status(404) 7 | if (req.accepts('html')) { 8 | return res.sendFile(path.join(__dirname, '..', 'views', '404.html')) 9 | } 10 | if (req.accepts('json')) { 11 | return res.json({ message: '404 page not found' }) 12 | } 13 | 14 | res.type('txt').send('404 page not found') 15 | }) 16 | module.exports = router 17 | -------------------------------------------------------------------------------- /backend/models/notification.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const notificationSchema = new mongoose.Schema( 4 | { 5 | user: { type: mongoose.Schema.Types.ObjectId, require: true }, 6 | title: { type: String, require: true }, 7 | type: { type: Number, required: true }, 8 | text: { type: String, require: true }, 9 | read: { type: Boolean, default: false }, 10 | }, 11 | { 12 | timestamps: true, 13 | } 14 | ) 15 | module.exports = mongoose.model('notification', notificationSchema) 16 | -------------------------------------------------------------------------------- /backend/routes/noteRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const noteController = require('../controllers/noteController') 4 | const verifyJWT = require('../middleware/verifyJWT') 5 | 6 | router.use(verifyJWT) 7 | router 8 | .route('/') 9 | .get(noteController.getAllNotes) 10 | .post(noteController.createNewNote) 11 | .patch(noteController.updateNote) 12 | .delete(noteController.deleteNote) 13 | 14 | router.route('/one').post(noteController.getOneNote) 15 | module.exports = router 16 | -------------------------------------------------------------------------------- /frontend/src/hooks/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | const useLocalStorage = (item, defaultValue) => { 4 | // Preventing useEffect from running twice 5 | const effectRef = useRef(false); 6 | const [persist, setPersist] = useState(JSON.parse(localStorage.getItem(item)) || defaultValue); 7 | useEffect(() => { 8 | localStorage.setItem(item, JSON.stringify(persist)); 9 | effectRef.current = true; 10 | }, [item, persist]); 11 | return [persist, setPersist]; 12 | }; 13 | export default useLocalStorage; 14 | -------------------------------------------------------------------------------- /frontend/src/components/DashLayout.js: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | import DashLayoutFooter from './DashLayoutFooter'; 3 | import DashLayoutHeader from './DashLayoutHeader'; 4 | 5 | export default function DashLayout() { 6 | return ( 7 | <> 8 |
14 | 15 |

Dash

16 | 17 |
18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /backend/config/corsConfigs.js: -------------------------------------------------------------------------------- 1 | const allowedOrigins = require('./allowedOrigins') 2 | 3 | // const port = process.env.PORT || 3500; 4 | const corsConfigs = { 5 | origin: (origin, callback) => { 6 | if (allowedOrigins.indexOf(origin) !== -1 || !origin) { 7 | // remove ||!origin to block postman request 8 | 9 | callback(null, true) 10 | } else { 11 | callback(new Error('origin not allowed by Cors')) 12 | } 13 | }, 14 | // origin: [`http://localhost:${port}`, `https://localhost:${port}`], 15 | credentials: true, 16 | optionsSuccessStatus: 200, 17 | } 18 | module.exports = corsConfigs 19 | -------------------------------------------------------------------------------- /frontend/src/components/users/NewUserFrom.module.css: -------------------------------------------------------------------------------- 1 | .form__control__container { 2 | display: flex; 3 | flex-direction: row; 4 | margin: 10px; 5 | padding: 5px; 6 | width: 300px; 7 | } 8 | 9 | .form__control__container label { 10 | width: 200px; 11 | } 12 | 13 | .form__control__container input, 14 | select { 15 | width: 400px; 16 | } 17 | 18 | .form__control__container button { 19 | width: 300px; 20 | } 21 | 22 | .form__container { 23 | display: flex; 24 | flex-direction: column; 25 | justify-content: center; 26 | align-items: center; 27 | flex: 1; 28 | border-radius: 3px; 29 | border-color: #cccc; 30 | border: 2px; 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/recoil/atom.js: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | import { recoilPersist } from 'recoil-persist'; 3 | 4 | const { persistAtom } = recoilPersist(); 5 | export const AccessToken = atom({ 6 | key: 'AccessToken', 7 | default: null 8 | }); 9 | export const NotificationsLength = atom({ 10 | key: 'NotificationsLength', 11 | default: 0 12 | }); 13 | export const Persist = atom({ 14 | key: 'Persist', 15 | default: false, 16 | effects_UNSTABLE: [persistAtom] 17 | // effects: [ 18 | // ({ setSelf }) => { 19 | // const cookies = new Cookies() 20 | // const checkToken = cookies.get('checkToken') 21 | // }, 22 | // ], 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/components/PageNotFound.module.css: -------------------------------------------------------------------------------- 1 | .notfound { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | flex-direction: column; 6 | position: absolute; 7 | top: 50%; 8 | left: 50%; 9 | transform: translate(-50%, -50%); 10 | } 11 | 12 | .notfound > h1 { 13 | font-family: Montserrat; 14 | font-size: 252px; 15 | font-weight: bold; 16 | letter-spacing: -40px; 17 | margin: 0; 18 | } 19 | 20 | .notfound > h1 > span { 21 | text-shadow: -8px 0 0 #fff; 22 | } 23 | 24 | @media only screen and (max-width: 480px) { 25 | .notfound > h1 { 26 | font-size: 126px; 27 | letter-spacing: -20px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/models/note.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const autoIncrement = require('mongoose-sequence')(mongoose) 3 | 4 | const noteSchema = new mongoose.Schema( 5 | { 6 | user: { type: mongoose.Schema.Types.ObjectId, require: true }, 7 | title: { type: String, require: true }, 8 | text: { type: String, require: true }, 9 | completed: { type: Boolean, default: false }, 10 | }, 11 | { 12 | timestamps: true, 13 | } 14 | ) 15 | noteSchema.plugin(autoIncrement, { 16 | inc_field: 'ticket', 17 | id: 'ticketNums', 18 | start_seq: 500, 19 | }) 20 | module.exports = mongoose.model('Note', noteSchema) 21 | -------------------------------------------------------------------------------- /backend/routes/notificationRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const notificationController = require('../controllers/notificationController'); 4 | const verifyJWT = require('../middleware/verifyJWT'); 5 | 6 | router.use(verifyJWT); 7 | router 8 | .route('/') 9 | .post(notificationController.getAllNotifications) 10 | .delete(notificationController.deleteNotification) 11 | .patch(notificationController.markOneNotificationasread); 12 | 13 | router 14 | .route('/all') 15 | .delete(notificationController.deleteAllNotifications) 16 | .patch(notificationController.markAllNotificationsAsRead); 17 | 18 | module.exports = router; 19 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :x: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 4 | import { RecoilRoot } from 'recoil'; 5 | import { disableReactDevTools } from '@fvilers/disable-react-devtools'; 6 | import App from './App'; 7 | 8 | if (process.env.NODE_ENV === 'production') { 9 | disableReactDevTools(); 10 | } 11 | const root = ReactDOM.createRoot(document.getElementById('root')); 12 | root.render( 13 | 14 | 15 | 16 | 17 | } /> 18 | 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /backend/middleware/verifyJWT.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | 3 | const verifyJWT = (req, res, next) => { 4 | const authHeader = req.headers.authorization || req.headers.Authorization 5 | if (!authHeader?.startsWith('Bearer ')) { 6 | return res.status(401).json({ message: 'Unauthorized r21831' }) 7 | } 8 | const token = authHeader.split(' ')[1] 9 | 10 | jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, decoded) => { 11 | if (err) { 12 | return res.status(403).json({ message: 'Forbidden r37226' }) 13 | } 14 | req.user = decoded.UserInfo.username 15 | req.roles = decoded.UserInfo.roles 16 | next() 17 | }) 18 | } 19 | module.exports = verifyJWT 20 | -------------------------------------------------------------------------------- /frontend/src/components/PageNotFound.js: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import styles from '../App.module.css'; 3 | import componentStyles from './PageNotFound.module.css'; 4 | 5 | export default function PageNotFound() { 6 | const navigate = useNavigate(); 7 | return ( 8 |
9 |

10 | 4 11 | 0 12 | 4 13 |

14 |
15 | 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /backend/middleware/loginLimiter.js: -------------------------------------------------------------------------------- 1 | const rateLimit = require('express-rate-limit') 2 | const { logEvents } = require('./logger') 3 | 4 | const loginLimiter = rateLimit({ 5 | windowMs: 60 * 1000, //1min limit 6 | max: 5, //5 request limit 7 | message: { 8 | message: 9 | 'too many attempts from this IP, please try later after 60 seconds', 10 | }, 11 | handler: (req, res, next, options) => { 12 | logEvents( 13 | `Too Many Requests: ${options.message.message}\t${req.method}\t${req.url}\t${req.header.origin}`, 14 | 'errLog.log' 15 | ) 16 | res.status(options.statusCode).send(options.message) 17 | }, 18 | standardHeaders: true, //return rate limit info in the "RateLimit-*" headers 19 | legacyHeaders: false, //Disable the "X-RateLimit-* headers 20 | }) 21 | module.exports = loginLimiter 22 | -------------------------------------------------------------------------------- /frontend/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { MdLogin } from 'react-icons/md'; 3 | import styles from '../App.module.css'; 4 | 5 | export default function Home() { 6 | const navigate = useNavigate(); 7 | return ( 8 | <> 9 |

Home

10 | 11 |

Welcome to MERN-auth-roles application

12 |

13 | A full-stack application that allows you to manage authentication and roles for users, using 14 | MERN and 15 |

16 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /backend/middleware/logger.js: -------------------------------------------------------------------------------- 1 | const { v4: uuid } = require('uuid') 2 | const fs = require('fs') 3 | const fsPromises = require('fs').promises 4 | const path = require('path') 5 | const { format } = require('date-fns') 6 | 7 | const logEvents = async (message, logFileName) => { 8 | const dateTime = `${format(new Date(), 'yyyyMMdd/tHH:mm:ss')}` 9 | const logItem = `${dateTime}\t${uuid()}\t${message}\n` 10 | try { 11 | if (!fs.existsSync(path.join(__dirname, '..', 'logs'))) { 12 | await fsPromises.mkdir(path.join(__dirname, '..', 'logs')) 13 | } 14 | await fsPromises.appendFile( 15 | path.join(__dirname, '..', 'logs', logFileName), 16 | logItem 17 | ) 18 | } catch { 19 | ;(err) => console.log('Error: ' + err) 20 | } 21 | } 22 | const logger = (req, res, next) => { 23 | const message = `${req.method}\t${req.url}\t${req.headers.origin}` 24 | logEvents(message, 'reqLog.log') 25 | console.log(`${req?.method} ${req?.path}`) 26 | next() 27 | } 28 | module.exports = { logger, logEvents } 29 | -------------------------------------------------------------------------------- /backend/socketio.js: -------------------------------------------------------------------------------- 1 | const user = require('./models/user'); 2 | const notification = require('./models/notification'); 3 | let usersio = []; 4 | 5 | module.exports = function (io) { 6 | io.on('connection', (socket) => { 7 | socket.on('setUserId', async (userId) => { 8 | if (userId) { 9 | const oneUser = await user.findById(userId).lean().exec(); 10 | if (oneUser) { 11 | usersio[userId] = socket; 12 | console.log(`⚡ Socket: User with id ${userId} connected`); 13 | } else { 14 | console.log(`🚩 Socket: No user with id ${userId}`); 15 | } 16 | } 17 | }); 18 | socket.on('getNotificationsLength', async (userId) => { 19 | const notifications = await notification 20 | .find({ user: userId, read: false }) 21 | .lean(); 22 | usersio[userId]?.emit('notificationsLength', notifications.length || 0); 23 | }); 24 | 25 | socket.on('disconnect', (userId) => { 26 | console.log(`🔥 user with id ${userId} disconnected from socket`); 27 | usersio[userId] = null; 28 | }); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 adelpro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-auth-roles-backend", 3 | "version": "4", 4 | "description": "MERN-auth-roles a web application that alow you to manage authentication and roles of users using MERN (MongoDB Express Angular Node)", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server", 8 | "dev": "nodemon server" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt": "^5.0.1", 15 | "cookie-parser": "^1.4.6", 16 | "cors": "^2.8.5", 17 | "date-fns": "^2.29.3", 18 | "dotenv": "^16.0.2", 19 | "express": "^4.18.1", 20 | "express-async-errors": "^3.1.1", 21 | "express-rate-limit": "^6.6.0", 22 | "jsonwebtoken": "^8.5.1", 23 | "jwt-decoder": "^0.0.0", 24 | "mongoose": "^6.6.1", 25 | "mongoose-sequence": "^5.3.1", 26 | "multer": "^1.4.5-lts.1", 27 | "nodemon": "^2.0.20", 28 | "socket.io": "^4.5.3", 29 | "uuid": "^9.0.0", 30 | "validator": "^13.7.0" 31 | }, 32 | "devDependencies": { 33 | "eslint": "^8.26.0", 34 | "eslint-config-prettier": "^8.5.0", 35 | "prettier": "^2.7.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/hooks/useAuth.js: -------------------------------------------------------------------------------- 1 | import { useRecoilValue } from 'recoil'; 2 | import jwtDecode from 'jwt-decode'; 3 | import { AccessToken } from '../recoil/atom'; 4 | 5 | const useAuth = () => { 6 | const accessToken = useRecoilValue(AccessToken); 7 | let isAdmin = false; 8 | let isManager = false; 9 | let status = 'Employee'; 10 | const defaultImage = `${process.env.REACT_APP_BASEURL}/images/default.png`; 11 | 12 | if (accessToken) { 13 | const decode = jwtDecode(accessToken); 14 | const { username, id, roles, profileImage } = decode.UserInfo; 15 | const loadProfileImage = 16 | profileImage?.length !== 0 ? `${process.env.REACT_APP_BASEURL + profileImage}` : defaultImage; 17 | isAdmin = roles.includes('Admin'); 18 | isManager = roles.includes('Manager'); 19 | if (isManager) status = 'Manager'; 20 | if (isAdmin) status = 'Admin'; 21 | return { 22 | username, 23 | roles, 24 | id, 25 | status, 26 | isAdmin, 27 | profileImage: loadProfileImage 28 | }; 29 | } 30 | return { 31 | username: '', 32 | id: null, 33 | roles: [], 34 | status, 35 | isAdmin, 36 | isManager, 37 | profileImage: defaultImage 38 | }; 39 | }; 40 | export default useAuth; 41 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "airbnb", 9 | "eslint:recommended", 10 | "plugin:react/recommended", 11 | "plugin:prettier/recommended" 12 | ], 13 | "overrides": [], 14 | "parserOptions": { 15 | "ecmaVersion": "latest", 16 | "sourceType": "module" 17 | }, 18 | 19 | "plugins": ["react"], 20 | "rules": { 21 | "react/react-in-jsx-scope": "off", 22 | "no-console": "off", 23 | "react/prop-types": "off", 24 | "react/jsx-props-no-spreading": "off", 25 | "no-underscore-dangle": ["error", { "allow": ["id", "_id"] }], 26 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 27 | "quotes": [2, "single", { "avoidEscape": true }], 28 | "prettier/prettier": ["error", { "singleQuote": true }, {"endOfLine": "auto"}], 29 | "jsx-a11y/label-has-associated-control": [ 30 | "error", 31 | { 32 | "required": { 33 | "some": ["nesting", "id"] 34 | } 35 | } 36 | ], 37 | "jsx-a11y/label-has-for": [ 38 | "error", 39 | { 40 | "required": { 41 | "some": ["nesting", "id"] 42 | } 43 | } 44 | ] 45 | }, 46 | "ignorePatterns": ["build/*"] 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/components/auth/PersistLogin.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { useRecoilState, useRecoilValue } from 'recoil'; 3 | import { Outlet, useLocation, useNavigate } from 'react-router-dom'; 4 | 5 | import { AccessToken, Persist } from '../../recoil/atom'; 6 | import useRefreshAccessToken from '../../hooks/useRefreshAccessToken'; 7 | 8 | export default function PersistLogin() { 9 | const [accessToken, setAccessToken] = useRecoilState(AccessToken); 10 | const persist = useRecoilValue(Persist); 11 | const getNewToken = useRefreshAccessToken(); 12 | const location = useLocation(); 13 | const navigate = useNavigate(); 14 | const effectRan = useRef(); 15 | useEffect(() => { 16 | if (effectRan.current === true || process.env.NODE_ENV !== 'development') { 17 | // React 18 Strict Mode 18 | const verifyRefreshToken = async () => { 19 | const newAccessToken = await getNewToken(); 20 | setAccessToken(newAccessToken); 21 | }; 22 | if (!accessToken && persist) { 23 | verifyRefreshToken(); 24 | } else if (!accessToken) { 25 | navigate('/', { 26 | replace: true 27 | }); 28 | } 29 | } 30 | effectRan.current = true; 31 | }, [accessToken, getNewToken, location, navigate, persist, setAccessToken]); 32 | return ; 33 | } 34 | -------------------------------------------------------------------------------- /backend/routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const userController = require('../controllers/userController') 4 | const verifyJWT = require('../middleware/verifyJWT') 5 | const multer = require('multer') 6 | 7 | const { v4: uuid } = require('uuid') 8 | 9 | const storage = multer.diskStorage({ 10 | destination: (req, file, cb) => { 11 | cb(null, './images/') 12 | }, 13 | filename: (req, file, cb) => { 14 | const fileName = 15 | 'profileimage' + 16 | uuid().toString() + 17 | '_' + 18 | file.originalname.toLowerCase().split(' ').join('-') 19 | cb(null, fileName) 20 | }, 21 | }) 22 | 23 | 24 | const fileFilter = (req, file, cb) => { 25 | if ( 26 | file.mimetype === 'image/jpeg' || 27 | file.mimetype === 'image/png' || 28 | file.mimetype == 'image/jpg' 29 | ) { 30 | cb(null, true) 31 | } else { 32 | cb('Only .png, .jpg and .jpeg image format allowed!', false) 33 | } 34 | } 35 | 36 | 37 | const upload = multer({ storage, fileFilter, limits: 1024 * 1024 * 5 }) 38 | 39 | router.route('/').post(userController.createNewUser) 40 | router.use(verifyJWT) 41 | router 42 | .route('/') 43 | .get(userController.getAllUsers) 44 | .patch(userController.updateUser) 45 | .delete(userController.deleteUser) 46 | router.route('/one').post(userController.getOneUser) 47 | router 48 | .route('/image') 49 | .post(upload.single('image'), userController.updateUserImage) 50 | module.exports = router 51 | -------------------------------------------------------------------------------- /frontend/src/hooks/useAxiosPrivate.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useRecoilState } from 'recoil'; 3 | import { axiosPrivate } from '../api/axios'; 4 | import useRefreshAccessToken from './useRefreshAccessToken'; 5 | import { AccessToken } from '../recoil/atom'; 6 | 7 | const useAxiosPrivate = () => { 8 | const [accessToken, setAccessToken] = useRecoilState(AccessToken); 9 | const getNewToken = useRefreshAccessToken(); 10 | useEffect(() => { 11 | const requestIntercept = axiosPrivate.interceptors.request.use( 12 | (config) => { 13 | const newConfig = config; 14 | if (!config.headers.Authorization && accessToken) { 15 | newConfig.headers.Authorization = `Bearer ${accessToken}`; 16 | } 17 | return newConfig; 18 | }, 19 | (error) => { 20 | Promise.reject(error); 21 | } 22 | ); 23 | const responseIntercept = axiosPrivate.interceptors.response.use( 24 | (response) => response, 25 | async (error) => { 26 | const prevRequest = error?.config; 27 | if (error?.response?.status === 403 && !prevRequest?.sent) { 28 | prevRequest.sent = true; 29 | const newAccessToken = await getNewToken(); 30 | 31 | setAccessToken(newAccessToken); 32 | 33 | prevRequest.headers.Authorization = `Bearer ${newAccessToken}`; 34 | return axiosPrivate(prevRequest); 35 | } 36 | return Promise.reject(error); 37 | } 38 | ); 39 | return () => { 40 | axiosPrivate.interceptors.request.eject(requestIntercept); 41 | axiosPrivate.interceptors.response.eject(responseIntercept); 42 | }; 43 | }, [accessToken, getNewToken, setAccessToken]); 44 | 45 | return axiosPrivate; 46 | }; 47 | export default useAxiosPrivate; 48 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-auth-roles-frontend", 3 | "version": "4", 4 | "description": "MERN-auth-roles a web application that alow you to manage authentication and roles of users using MERN (MongoDB Express Angular Node)", 5 | "dependencies": { 6 | "@fvilers/disable-react-devtools": "^1.3.0", 7 | "@hookform/resolvers": "^2.9.8", 8 | "@uiball/loaders": "^1.2.6", 9 | "axios": "^1.1.2", 10 | "jwt-decode": "^3.1.2", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-hook-form": "^7.36.0", 14 | "react-icons": "^4.4.0", 15 | "react-multi-select-component": "^4.3.4", 16 | "react-router-dom": "^6.3.0", 17 | "react-scripts": "^5.0.1", 18 | "recoil": "^0.7.5", 19 | "recoil-persist": "^4.2.0", 20 | "socket.io-client": "^4.5.3", 21 | "yup": "^0.32.11" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject", 28 | "lint": "eslint .", 29 | "lint:fix": "eslint --fix", 30 | "format": "prettier --write './**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc.json" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | }, 44 | "devDependencies": { 45 | "eslint": "^8.27.0", 46 | "eslint-config-airbnb": "^19.0.4", 47 | "eslint-config-prettier": "^8.5.0", 48 | "eslint-plugin-import": "^2.26.0", 49 | "eslint-plugin-jsx-a11y": "^6.6.1", 50 | "eslint-plugin-prettier": "^4.2.1", 51 | "eslint-plugin-react": "^7.31.10", 52 | "eslint-plugin-react-hooks": "^4.6.0", 53 | "prettier": "^2.7.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | MERN-auth-roles 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | require('express-async-errors') 3 | const express = require('express') 4 | const path = require('path') 5 | const { logger, logEvents } = require('./middleware/logger') 6 | const errorHandler = require('./middleware/errorHandler') 7 | const cookieParser = require('cookie-parser') 8 | const cors = require('cors') 9 | const corsConfigs = require('./config/corsConfigs') 10 | const allowedOrigins = require('./config/allowedOrigins') 11 | const mongoose = require('mongoose') 12 | const connectDB = require('./config/dbConn') 13 | const credentials = require('./middleware/credentials') 14 | const app = express() 15 | const port = process.env.PORT || 3500 16 | 17 | connectDB() 18 | 19 | app.use(logger) 20 | app.use(credentials) 21 | app.use(cors(corsConfigs)) 22 | app.use(express.json()) 23 | app.use(cookieParser()) 24 | app.use('/', express.static(path.join(__dirname, '/views'))) 25 | app.use('/images', express.static('images')) 26 | app.use('/', require('./routes/root')) 27 | 28 | // Socketio must be declared before api routes 29 | const server = require('http').createServer(app) 30 | const io = require('socket.io')(server, { 31 | transports: ['polling'], 32 | cors: { origin: allowedOrigins }, 33 | }) 34 | require('./socketio.js')(io) 35 | app.use('/users', require('./routes/userRoutes')) 36 | app.use('/notes', require('./routes/noteRoutes')) 37 | app.use('/auth', require('./routes/authRoutes')) 38 | app.use('/notifications', require('./routes/notificationRoutes')) 39 | app.all('*', require('./routes/404')) 40 | 41 | app.use(errorHandler) 42 | 43 | mongoose.connection.once('open', () => { 44 | server.listen(port, () => { 45 | console.log('🔗 Successfully Connected to MongoDB') 46 | console.log(`✅ Application running on port: ${port}`); 47 | }) 48 | }) 49 | mongoose.connection.on('error', (err) => { 50 | // TODO send notification to all admins by saving notification in with each admin id 51 | console.log(err) 52 | logEvents( 53 | `${err.no}: ${err.code}\t${err.syscall}\t${err.hostname}\t`, 54 | 'mongoDBErrLog.log' 55 | ) 56 | }) 57 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom'; 2 | import Home from './components/Home'; 3 | import Login from './components/auth/Login'; 4 | import DashLayout from './components/DashLayout'; 5 | import PageNotFound from './components/PageNotFound'; 6 | import Welcome from './components/auth/Welcome'; 7 | import NotesList from './components/notes/NotesList'; 8 | import UsersList from './components/users/UsersList'; 9 | import NewUserFrom from './components/users/NewUserFrom'; 10 | import PersistLogin from './components/auth/PersistLogin'; 11 | import ProtectedRoutes from './components/ProtectedRoutes'; 12 | import ROLES from './config/roles'; 13 | import EditUserForm from './components/users/EditUserForm'; 14 | import NewNoteForm from './components/notes/NewNoteForm'; 15 | import EditNoteForm from './components/notes/EditNoteForm'; 16 | import Profile from './components/users/Profile'; 17 | import NotificationsList from './components/notifications/NotificationsList'; 18 | 19 | function App() { 20 | return ( 21 | 22 | {/* Public routes */} 23 | } /> 24 | } /> 25 | {/* Private routes */} 26 | }> 27 | }> 28 | } /> 29 | 30 | } /> 31 | 32 | 33 | } /> 34 | 35 | 36 | } /> 37 | } /> 38 | } /> 39 | 40 | }> 41 | 42 | } /> 43 | }> 44 | } /> 45 | } /> 46 | 47 | 48 | 49 | 50 | 51 | } /> 52 | 53 | ); 54 | } 55 | 56 | export default App; 57 | -------------------------------------------------------------------------------- /frontend/src/components/auth/Welcome.js: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { useSetRecoilState, useRecoilValue } from 'recoil'; 3 | import { MdArrowForwardIos } from 'react-icons/md'; 4 | import { useEffect } from 'react'; 5 | import useAuth from '../../hooks/useAuth'; 6 | import { AccessToken, NotificationsLength } from '../../recoil/atom'; 7 | import styles from '../../App.module.css'; 8 | import useSocketIo from '../../hooks/useSocketIo'; 9 | 10 | export default function Welcome() { 11 | const accessToken = useRecoilValue(AccessToken); 12 | const { id, username, status, isAdmin } = useAuth(); 13 | const { socket } = useSocketIo(); 14 | const setNotificationsLength = useSetRecoilState(NotificationsLength); 15 | useEffect(() => { 16 | let timer; 17 | socket?.on('connect', () => { 18 | socket.emit('setUserId', id); 19 | // getting first notifications length 20 | socket.emit('getNotificationsLength', id); 21 | socket?.on('notificationsLength', (data) => { 22 | setNotificationsLength(data); 23 | }); 24 | timer = setTimeout(() => { 25 | socket.emit('getNotificationsLength', id); 26 | }, 10000); // run every 10 seconds 27 | socket?.on('disconnect', () => {}); 28 | }); 29 | 30 | return () => { 31 | socket?.off('connect'); 32 | socket?.off('disconnect'); 33 | socket?.off('notifications'); 34 | clearTimeout(timer); 35 | }; 36 | }, [id, socket]); 37 | 38 | return ( 39 |
45 |

46 | Welcome {username} [ {status} ] 47 |

48 |

49 | accessToken: 50 | {accessToken} 51 |

52 | 53 |

54 | Current ID: 55 | {id} 56 |

57 | Current server: 58 | {process.env.REACT_APP_BASEURL || 'http://localhost:3500'} 59 | 60 |
    61 |
  • 62 |
    63 | 64 | View Notes 65 |
    66 |
  • 67 | {isAdmin && ( 68 |
  • 69 |
    70 | 71 | View Users 72 |
    73 |
  • 74 | )} 75 |
76 |
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /frontend/src/App.module.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@200;500&display=swap'); 2 | 3 | body { 4 | font-family: Montserrat, Arial, sans-serif; 5 | } 6 | 7 | .form__container { 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | background-color: #f8ede3; 13 | width: 450px; 14 | border-radius: 10px; 15 | margin-bottom: 30px; 16 | box-sizing: border-box; 17 | padding: 5px; 18 | } 19 | 20 | .form__control__container { 21 | display: flex; 22 | flex-direction: row; 23 | margin: 10px; 24 | padding: 5px; 25 | } 26 | 27 | .form__control__container__checkbox { 28 | display: flex; 29 | flex-direction: row; 30 | margin: 10px; 31 | padding: 5px; 32 | font-size: 15px; 33 | } 34 | 35 | .form__control__container__checkbox > label { 36 | width: 200px; 37 | } 38 | 39 | section { 40 | display: flex; 41 | margin: 30px; 42 | background-color: #7d6e83; 43 | flex-direction: column; 44 | justify-content: center; 45 | align-items: center; 46 | flex: 1; 47 | font-family: 'Montserrat', sans-serif; 48 | font-size: larger; 49 | } 50 | 51 | input, 52 | label { 53 | margin: 2px; 54 | padding: 3px; 55 | width: 100px; 56 | height: 30px; 57 | border-radius: 5px; 58 | } 59 | 60 | textarea, 61 | input[type='text'], 62 | input[type='password'] { 63 | width: 200px; 64 | font-size: 1rem; 65 | } 66 | 67 | h1 { 68 | font-size: 2rem; 69 | font-weight: bold; 70 | } 71 | 72 | .error { 73 | margin: 3px; 74 | text-align: center; 75 | font-family: 'Montserrat', sans-serif; 76 | color: #802a36; 77 | } 78 | 79 | .button { 80 | margin: 10px; 81 | width: 90px; 82 | height: 40px; 83 | border-radius: 5px; 84 | color: #dff6ff; 85 | background-color: #06283d; 86 | cursor: pointer; 87 | text-align: 'center'; 88 | cursor: pointer; 89 | /*grow effect*/ 90 | transition: all 0.1s ease-in-out; 91 | } 92 | 93 | .button:hover { 94 | background-color: #083755; 95 | font-weight: bold; 96 | border-color: rgba(88, 88, 88, 0.8); 97 | /*grow effect*/ 98 | transform: scale(1.1); 99 | } 100 | .button:disabled { background-color: #b2cee0;color:whitesmoke} 101 | .list li { 102 | display: flex; 103 | justify-content: space-between; 104 | align-items: center; 105 | align-content: center; 106 | width: 90%; 107 | height: 50px; 108 | margin: 7px; 109 | padding: 2px 2px 2px 20px; 110 | border-radius: 10px; 111 | border: 1px solid #ccc; 112 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); 113 | transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1); 114 | } 115 | 116 | .list li:hover { 117 | transform: scale(1.02, 1.2); 118 | } 119 | 120 | .list li button { 121 | width: 30px; 122 | height: 30px; 123 | margin: 2px; 124 | border-radius: 4px; 125 | cursor: pointer; 126 | } 127 | 128 | .list li button:hover { 129 | background-color: rgba(156, 156, 156, 0.8); 130 | border-color: #06283d; 131 | border-width: 2; 132 | } 133 | 134 | .list li::marker { 135 | content: '✓ '; 136 | color: #083755; 137 | } 138 | 139 | .dashed { 140 | border-top: 3px dashed #bbb; 141 | } 142 | 143 | .center { 144 | display: flex; 145 | justify-content: center; 146 | align-items: center; 147 | } 148 | 149 | @media only screen and (max-width: 480px) { 150 | section { 151 | margin: 0; 152 | } 153 | 154 | .form__control__container { 155 | margin: 2px; 156 | } 157 | 158 | .form__container { 159 | width: 350px; 160 | } 161 | 162 | .list { 163 | padding: 2px; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /frontend/src/components/DashLayoutHeader.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useLocation, useNavigate } from 'react-router-dom'; 3 | import { useRecoilValue, useSetRecoilState } from 'recoil'; 4 | import { 5 | MdOutlinePersonOutline, 6 | MdLogout, 7 | MdDashboard, 8 | MdNotificationsNone, 9 | MdNotificationsActive 10 | } from 'react-icons/md'; 11 | import { Ring } from '@uiball/loaders'; 12 | import { AccessToken, NotificationsLength } from '../recoil/atom'; 13 | import axios from '../api/axios'; 14 | import styles from '../App.module.css'; 15 | import dashstyles from './DashLayoutHeader.module.css'; 16 | 17 | export default function DashLayoutHeader() { 18 | const navigate = useNavigate(); 19 | const location = useLocation(); 20 | const setAccessToken = useSetRecoilState(AccessToken); 21 | const notificationsLength = useRecoilValue(NotificationsLength); 22 | const [isloading, setIsloading] = useState(false); 23 | const logoutHandler = async () => { 24 | // send logout request 25 | setIsloading(true); 26 | await axios 27 | .post('/auth/logout') 28 | .then(() => { 29 | setAccessToken(null); 30 | setIsloading(false); 31 | navigate('/'); 32 | }) 33 | .catch(() => { 34 | setIsloading(false); 35 | }); 36 | }; 37 | return ( 38 |
39 |

MERN - auth - roles

40 |
41 | 54 | 67 | 84 | 100 |
101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /backend/controllers/noteController.js: -------------------------------------------------------------------------------- 1 | const note = require('../models/note') 2 | 3 | // @desc Get all notes 4 | // @Route GET /notes 5 | // @Access Private 6 | const getAllNotes = async (req, res) => { 7 | const notes = await note.find().lean() 8 | if (!notes) { 9 | return res.status(400).json({ message: 'No Notes found' }) 10 | } 11 | res.json(notes) 12 | } 13 | 14 | // @desc Create new note 15 | // @Route POST /notes 16 | // @Access Private 17 | const createNewNote = async (req, res) => { 18 | const { user, title, text } = req.body 19 | //Confirm data 20 | if (!user.match(/^[0-9a-fA-F]{24}$/) || !title || !text) { 21 | return res 22 | .status(400) 23 | .json({ message: 'Verify your data and proceed again' }) 24 | } 25 | //create new note 26 | const newNote = await note.create({ 27 | user, 28 | title, 29 | text, 30 | }) 31 | if (newNote) { 32 | res.json({ 33 | message: `New note with title: ${title} created with success`, 34 | }) 35 | } else { 36 | res.status(400).json({ 37 | message: 'Note creation failed, please verify your data and try again', 38 | }) 39 | } 40 | } 41 | 42 | // @desc Get a note by id 43 | // @Route PATCH /notes/one 44 | // @Access Private 45 | const getOneNote = async (req, res) => { 46 | const { id } = req.body 47 | //Confirm data 48 | 49 | if (!id) { 50 | return res 51 | .status(400) 52 | .json({ message: 'Verify your data and proceed again r35475' }) 53 | } 54 | if (!id || !id.match(/^[0-9a-fA-F]{24}$/)) { 55 | return res.status(400).json({ message: `You must give a valid id: ${id}` }) 56 | } 57 | // Check if the note exist 58 | const oneNote = await note.findById(id).lean() 59 | if (!oneNote) { 60 | return res 61 | .status(400) 62 | .json({ message: `Can't find a note with this id: ${id}` }) 63 | } 64 | res.json(oneNote) 65 | } 66 | 67 | // @desc Update a note 68 | // @Route PATCH /notes 69 | // @Access Private 70 | const updateNote = async (req, res) => { 71 | const { id, title, text, completed } = req.body 72 | //Confirm data 73 | if ( 74 | !id || 75 | !title || 76 | !text || 77 | text.length < 10 || 78 | typeof completed !== 'boolean' 79 | ) { 80 | return res 81 | .status(400) 82 | .json({ message: 'Verify your data and proceed again' }) 83 | } 84 | // Check if the note exist 85 | const updateNote = await note.findById(id).exec() 86 | if (!updateNote) { 87 | return res 88 | .status(400) 89 | .json({ message: `Can't find a note with this id: ${id}` }) 90 | } 91 | updateNote.title = title 92 | updateNote.text = text 93 | updateNote.completed = completed 94 | await updateNote.save() 95 | res.json({ message: `Note with title: ${title} updated with success` }) 96 | } 97 | 98 | // @desc delete a note 99 | // @Route DELETE /notes 100 | // @Private access 101 | const deleteNote = async (req, res) => { 102 | const { id } = req.body 103 | if (!id || !id.match(/^[0-9a-fA-F]{24}$/)) { 104 | return res.status(400).json({ message: `You must give a valid id: ${id}` }) 105 | } 106 | 107 | const deleteNote = await note.findById(id).exec() 108 | if (!deleteNote) { 109 | return res.status(400).json({ message: `Can't find a note with id: ${id}` }) 110 | } 111 | const result = await deleteNote.deleteOne() 112 | if (!result) { 113 | return res 114 | .status(400) 115 | .json({ message: `Can't delete the note with id: ${id}` }) 116 | } 117 | res.json({ message: `Note with id: ${id} deleted with success` }) 118 | } 119 | module.exports = { 120 | createNewNote, 121 | updateNote, 122 | getAllNotes, 123 | getOneNote, 124 | deleteNote, 125 | } 126 | -------------------------------------------------------------------------------- /backend/controllers/authController.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const User = require('../models/user'); 3 | const notification = require('../models/notification'); 4 | const jwt = require('jsonwebtoken'); 5 | 6 | // @desc Login 7 | // @Route POST /auth 8 | // @Access Public 9 | const login = async (req, res) => { 10 | const { username, password } = req.body; 11 | if (!username || !password) { 12 | return res.status(400).json({ message: 'All fields are required' }); 13 | } 14 | const foundUser = await User.findOne({ username }).exec(); 15 | if (!foundUser || !foundUser.active) { 16 | res.status(401).json({ message: 'Unauthorized r24157' }); 17 | } 18 | const matchPassword = await bcrypt.compare(password, foundUser.password); 19 | if (!matchPassword) { 20 | res.status(401).json({ message: 'Unauthorized r97452' }); 21 | } 22 | const accessToken = jwt.sign( 23 | { 24 | UserInfo: { 25 | username: foundUser.username, 26 | id: foundUser._id, 27 | roles: foundUser.roles, 28 | profileImage: foundUser.profileImage, 29 | }, 30 | }, 31 | process.env.ACCESS_TOKEN_SECRET, 32 | { expiresIn: '30s' } 33 | ); 34 | const refreshToken = jwt.sign( 35 | { 36 | UserInfo: { 37 | username: foundUser.username, 38 | id: foundUser._id, 39 | roles: foundUser.roles, 40 | profileImage: foundUser.profileImage, 41 | }, 42 | }, 43 | process.env.REFRESH_TOKEN_SECRET, 44 | { expiresIn: '7d' } 45 | ); 46 | 47 | //Create secure cookie with refresh token 48 | res.cookie('jwt', refreshToken, { 49 | httpOnly: true, 50 | SameSite: 'None', 51 | secure: process.env.NODE_ENV === 'production', //-only for server with https 52 | maxAge: 24 * 60 * 60 * 1000, 53 | }); 54 | 55 | //then send access token with username and roles 56 | res.json({ accessToken }); 57 | 58 | // add notification for login 59 | await notification.create({ 60 | user: foundUser._id, 61 | title: 'login', 62 | type: 1, 63 | text: `New login at ${new Date()}`, 64 | read: false, 65 | }); 66 | }; 67 | 68 | // @desc Refresh 69 | // @Route get /auth/refresh 70 | // @Access Public 71 | const refresh = async (req, res) => { 72 | const cookies = req.cookies; 73 | if (!cookies?.jwt) { 74 | return res.status(401).json({ message: 'Unauthorized r65472' }); 75 | } 76 | 77 | const refreshToken = cookies.jwt; 78 | jwt.verify( 79 | refreshToken, 80 | process.env.REFRESH_TOKEN_SECRET, 81 | async (err, decoded) => { 82 | if (err) { 83 | return res.status(403).json({ message: 'Forbidden r74690' }); 84 | } 85 | const foundUser = await User.findOne({ 86 | username: decoded.UserInfo.username, 87 | }).exec(); 88 | if (!foundUser) { 89 | return res.status(401).json({ message: 'Unauthorized r68457' }); 90 | } 91 | const accessToken = jwt.sign( 92 | { 93 | UserInfo: { 94 | username: foundUser.username, 95 | id: foundUser._id, 96 | roles: foundUser.roles, 97 | profileImage: foundUser.profileImage, 98 | }, 99 | }, 100 | process.env.ACCESS_TOKEN_SECRET, 101 | { expiresIn: '7d' } 102 | ); 103 | //Send accessToken with username and roles 104 | res.json({ accessToken }); 105 | } 106 | ); 107 | }; 108 | 109 | // @desc Logout 110 | // @Route POST /auth/logout 111 | // @Access Public 112 | const logout = async (req, res) => { 113 | const cookies = req?.cookies; 114 | if (!cookies?.jwt) { 115 | return res.sendStatus(204); //No content 116 | } 117 | res.clearCookie('jwt', { httpOnly: true, samSite: 'None', secure: true }); 118 | }; 119 | 120 | module.exports = { login, refresh, logout }; 121 | -------------------------------------------------------------------------------- /backend/controllers/notificationController.js: -------------------------------------------------------------------------------- 1 | const notification = require('../models/notification'); 2 | 3 | // @desc Get all notifications 4 | // @Route GET /notes 5 | // @Access Private 6 | const getAllNotifications = async (req, res) => { 7 | const { id, page, limit } = req.body; 8 | const filtredNotifications = notification.find({ user: id }); 9 | const total = await filtredNotifications.countDocuments(); 10 | const notifications = await notification 11 | .find({ user: id }) 12 | .limit(limit) 13 | .skip(limit * page) 14 | .lean(); 15 | if (!notifications) { 16 | return res.status(400).json({ message: 'No notifications found' }); 17 | } 18 | console.log('total: ', total); 19 | 20 | res.json({ totalpage: Math.ceil(total / limit), notifications }); 21 | }; 22 | 23 | // @desc delete a notification 24 | // @Route DELETE /notifications 25 | // @Private access 26 | const deleteNotification = async (req, res) => { 27 | const { id } = req.body; 28 | if (!id || !id.match(/^[0-9a-fA-F]{24}$/)) { 29 | return res.status(400).json({ message: `You must give a valid id: ${id}` }); 30 | } 31 | 32 | const deleteNotification = await notification.findById(id).exec(); 33 | if (!deleteNotification) { 34 | return res 35 | .status(400) 36 | .json({ message: `Can't find a notification with id: ${id}` }); 37 | } 38 | const result = await deleteNotification.deleteOne(); 39 | if (!result) { 40 | return res 41 | .status(400) 42 | .json({ message: `Can't delete the notification with id: ${id}` }); 43 | } 44 | res.json({ message: `Notification with id: ${id} deleted with success` }); 45 | }; 46 | 47 | // @desc delete All notification 48 | // @Route DELETE /notifications/all 49 | // @Private access 50 | const deleteAllNotifications = async (req, res) => { 51 | const { id } = req.body; 52 | if (!id || !id.match(/^[0-9a-fA-F]{24}$/)) { 53 | return res.status(400).json({ message: `You must give a valid id: ${id}` }); 54 | } 55 | const notificationsDeleteMany = await notification.deleteMany({ user: id }); 56 | if (!notificationsDeleteMany) { 57 | return res 58 | .status(400) 59 | .json({ message: 'Error Deleting all notifications as read' }); 60 | } 61 | res.json({ message: `All notifications for user ${id}marked was deleted` }); 62 | }; 63 | // @desc Mark One Notification As Read 64 | // @Route Patch /notifications/ 65 | // @Access Private 66 | const markOneNotificationasread = async (req, res) => { 67 | const { id } = req.body; 68 | if (!id || !id.match(/^[0-9a-fA-F]{24}$/)) { 69 | return res.status(400).json({ message: `You must give a valid id: ${id}` }); 70 | } 71 | const updateNotification = await notification.find({ id }).exec(); 72 | if (!updateNotification) { 73 | return res.status(400).json({ message: 'No notifications found' }); 74 | } 75 | updateNotification.read = false; 76 | await updateNotification.save(); 77 | res.json(updateNotification); 78 | }; 79 | // @desc Mark All Notifications As Read 80 | // @Route Patch /notifications/All 81 | // @Access Private 82 | const markAllNotificationsAsRead = async (req, res) => { 83 | const { id } = req.body; 84 | if (!id || !id.match(/^[0-9a-fA-F]{24}$/)) { 85 | return res.status(400).json({ message: `You must give a valid id: ${id}` }); 86 | } 87 | const notificationsUpdateMany = await notification.updateMany( 88 | { user: id }, 89 | { $set: { read: true } } 90 | ); 91 | if (!notificationsUpdateMany) { 92 | return res 93 | .status(400) 94 | .json({ message: 'Error Marking all notifications as read' }); 95 | } 96 | res.json({ message: `All notifications for user ${id}marked as read` }); 97 | }; 98 | module.exports = { 99 | getAllNotifications, 100 | deleteNotification, 101 | deleteAllNotifications, 102 | markOneNotificationasread, 103 | markAllNotificationsAsRead, 104 | }; 105 | -------------------------------------------------------------------------------- /frontend/src/components/users/UsersList.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { MdArrowForwardIos, MdDeleteOutline, MdBorderColor, MdAdd } from 'react-icons/md'; 4 | import { useRecoilValue } from 'recoil'; 5 | import styles from '../../App.module.css'; 6 | import useAxiosPrivate from '../../hooks/useAxiosPrivate'; 7 | import { AccessToken } from '../../recoil/atom'; 8 | 9 | export default function UsersList() { 10 | const accessToken = useRecoilValue(AccessToken); 11 | const axiosPrivate = useAxiosPrivate(); 12 | const [data, setData] = useState(null); 13 | const [isLoading, setIsloading] = useState(false); 14 | const [error, setError] = useState(null); 15 | const [message, setMessage] = useState(null); 16 | const messageRef = useRef(); 17 | const navigate = useNavigate(); 18 | useEffect(() => { 19 | setIsloading(true); 20 | const controller = new AbortController(); 21 | const getUsers = async () => { 22 | try { 23 | const result = await axiosPrivate.get('/users', { 24 | signal: controller.signal 25 | }); 26 | setData(result?.data); 27 | setError(null); 28 | setIsloading(false); 29 | } catch (err) { 30 | setData(null); 31 | setIsloading(false); 32 | setError(err?.response?.message); 33 | } 34 | }; 35 | 36 | if (accessToken) { 37 | getUsers(); 38 | } 39 | return () => { 40 | controller?.abort(); 41 | }; 42 | }, [axiosPrivate, accessToken]); 43 | 44 | const handleDeleteUser = async (id) => { 45 | try { 46 | const result = await axiosPrivate.delete('/users', { data: { id } }); 47 | setData(() => data.filter((item) => item._id !== id)); 48 | setMessage(result?.data?.message); 49 | messageRef.current.focus(); 50 | } catch (err) { 51 | setMessage(err?.response?.data?.message); 52 | messageRef.current.focus(); 53 | } 54 | }; 55 | if (error) return
{error.message}
; 56 | if (isLoading) return
Loading...
; 57 | let content =
No data to show
; 58 | if (data && data.length > 0) { 59 | content = ( 60 |
    61 | {data.map((user) => { 62 | return ( 63 | // eslint-disable-next-line no-underscore-dangle 64 |
    65 |
  • 66 |
    67 | 68 | {user.username} 69 | 70 | [ 71 | {user.roles.map((role, i, roles) => 72 | i + 1 === roles.length ? ( 73 | {role} 74 | ) : ( 75 | {role} , 76 | ) 77 | )} 78 | ] 79 | 80 |
    81 |
    82 | 85 | 88 |
    89 |
  • 90 |
    91 | ); 92 | })} 93 |
94 | ); 95 | } 96 | 97 | return ( 98 | <> 99 |

Users list

100 | {content} 101 |
102 | 111 |
112 |

113 | {message?.message} 114 |

115 | 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /frontend/src/components/notes/NotesList.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { MdArrowForwardIos, MdDeleteOutline, MdBorderColor, MdAdd } from 'react-icons/md'; 4 | import { useRecoilValue } from 'recoil'; 5 | import styles from '../../App.module.css'; 6 | import useAxiosPrivate from '../../hooks/useAxiosPrivate'; 7 | import { AccessToken } from '../../recoil/atom'; 8 | 9 | export default function NotesList() { 10 | const accessToken = useRecoilValue(AccessToken); 11 | const axiosPrivate = useAxiosPrivate(); 12 | const [data, setData] = useState(null); 13 | const [isLoading, setIsloading] = useState(false); 14 | const [error, setError] = useState(null); 15 | const [message, setMessage] = useState(null); 16 | const messageRef = useRef(); 17 | const navigate = useNavigate(); 18 | useEffect(() => { 19 | setIsloading(true); 20 | const controller = new AbortController(); 21 | const getNotes = async () => { 22 | try { 23 | const result = await axiosPrivate.get('/notes', { 24 | signal: controller.signal 25 | }); 26 | setData(result?.data); 27 | setError(null); 28 | } catch (err) { 29 | setData(null); 30 | setError(err?.response?.message); 31 | } finally { 32 | setIsloading(false); 33 | } 34 | }; 35 | if (accessToken) getNotes(); 36 | return () => { 37 | controller?.abort(); 38 | }; 39 | }, [axiosPrivate, accessToken]); 40 | 41 | const handleDeleteNote = async (id) => { 42 | try { 43 | const result = await axiosPrivate.delete('/notes', { data: { id } }); 44 | setData(() => data.filter((item) => item._id !== id)); 45 | setMessage(result?.data?.message); 46 | messageRef.current.focus(); 47 | } catch (err) { 48 | setMessage(err?.response?.data?.message); 49 | messageRef.current.focus(); 50 | } 51 | }; 52 | if (error) return
{error.message}
; 53 | if (isLoading) return
Loading...
; 54 | let content = ( 55 | <> 56 |

Notes list

57 |
No data to show
58 | 59 | ); 60 | 61 | if (data && data.length > 0) { 62 | content = ( 63 | <> 64 |

Notes list

65 |
    66 | {data.map((note) => { 67 | return ( 68 | // eslint-disable-next-line no-underscore-dangle 69 |
    70 |
  • 71 |
    72 |

    73 | 74 | {note.title} 75 |

    76 |
    77 |

    {note.text}

    78 | {note.completed ?

    ✅ Completed

    :

    ❌ Not completed

    } 79 |
    80 |
    81 | {/* eslint-disable-next-line no-underscore-dangle */} 82 | 85 | {/* eslint-disable-next-line no-underscore-dangle */} 86 | 89 |
    90 |
  • 91 |
    92 | ); 93 | })} 94 |
95 | 96 | ); 97 | } 98 | // Render data 99 | return ( 100 | <> 101 | {content} 102 |
103 | 112 |
113 |

114 | {message?.message} 115 |

116 | 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /frontend/src/components/notes/NewNoteForm.js: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | import { yupResolver } from '@hookform/resolvers/yup'; 4 | import * as yup from 'yup'; 5 | import { MdAdd, MdAutorenew, MdNoteAdd } from 'react-icons/md'; 6 | import { Ring } from '@uiball/loaders'; 7 | import { useLocation, useNavigate } from 'react-router-dom'; 8 | import styles from '../../App.module.css'; 9 | import useAuth from '../../hooks/useAuth'; 10 | import useAxiosPrivate from '../../hooks/useAxiosPrivate'; 11 | 12 | export default function NewNoteForm() { 13 | const navigate = useNavigate(); 14 | const location = useLocation(); 15 | const { id } = useAuth(); 16 | const axiosPrivate = useAxiosPrivate(); 17 | const messageRef = useRef(); 18 | const [isloading, setIsloading] = useState(false); 19 | const [message, setMessage] = useState(null); 20 | const schema = yup.object().shape({ 21 | title: yup.string().min(4).required('Title is required'), 22 | text: yup.string().min(10).required('Text is required') 23 | }); 24 | const { 25 | register, 26 | handleSubmit, 27 | reset, 28 | formState: { errors } 29 | } = useForm({ 30 | resolver: yupResolver(schema) 31 | }); 32 | const onSubmit = async (data) => { 33 | setIsloading(true); 34 | setMessage(null); 35 | await axiosPrivate 36 | .post('/notes', { ...data, user: id }) 37 | .then((result) => { 38 | setIsloading(false); 39 | setMessage(result?.data?.message); 40 | navigate(location.state?.from?.pathname || '/dash/notes', { 41 | replace: true 42 | }); 43 | }) 44 | .catch((err) => { 45 | if (!err?.response?.status) { 46 | setMessage(err?.response?.statusText ? err?.response?.statusText : 'No server response'); 47 | } else if (err?.response?.status === 400) { 48 | setMessage('Verify your data and proceed again'); 49 | } else { 50 | setMessage(err?.response?.statusText); 51 | } 52 | setIsloading(false); 53 | }); 54 | }; 55 | 56 | if (isloading) return

Loading ...

; 57 | return ( 58 |
59 |
65 | 66 |

Add new note

67 |
68 |
69 |
70 | 71 | 72 |
73 | {errors?.title &&

{errors?.title?.message}

} 74 |
75 | 76 |