├── 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 |
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 |
114 |
115 | );
116 | }
117 |
--------------------------------------------------------------------------------
/frontend/src/components/users/Profile.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 | import { MdOutlinePersonOutline, MdFileUpload } from 'react-icons/md';
3 | import { Ring } from '@uiball/loaders';
4 | import useAxiosPrivate from '../../hooks/useAxiosPrivate';
5 | import styles from '../../App.module.css';
6 | import useAuth from '../../hooks/useAuth';
7 |
8 | export default function Profile() {
9 | const axiosPrivate = useAxiosPrivate();
10 | const { username, roles, profileImage, id } = useAuth();
11 | const [image, setImage] = useState({
12 | preview: null,
13 | data: null
14 | });
15 | const [isloading, setIsloading] = useState(false);
16 | const [message, setMessage] = useState('');
17 | const messageRef = useRef();
18 | useEffect(() => {
19 | setImage((prev) => ({ ...prev, preview: profileImage }));
20 | }, [profileImage]);
21 | const handleSubmit = async (e) => {
22 | e.preventDefault();
23 | setIsloading(true);
24 | setMessage(null);
25 | const formData = new FormData();
26 | formData.append('image', image.data);
27 | formData.append('id', id);
28 | await axiosPrivate
29 | .post('/users/image', formData, {
30 | headers: {
31 | 'Content-Type': 'multipart/form-data'
32 | }
33 | })
34 | .then((result) => {
35 | setIsloading(false);
36 | setMessage(result?.response?.message);
37 | })
38 | .catch((err) => {
39 | setIsloading(false);
40 | if (!err?.response?.status) {
41 | setMessage(err?.response?.statusText ? err?.response?.statusText : 'No server response');
42 | } else if (err?.response?.status === 409) {
43 | setMessage('Username exist already');
44 | } else if (err?.response?.status === 401) {
45 | setMessage('Unauthorized');
46 | } else {
47 | setMessage(err?.response?.statusText);
48 | }
49 | });
50 | };
51 |
52 | const handleFileChange = (e) => {
53 | const img = {
54 | preview: URL.createObjectURL(e.target.files[0]),
55 | data: e.target.files[0]
56 | };
57 | setImage(img);
58 | };
59 | return (
60 |
61 |
62 |
63 |
64 |
Profile
65 |
66 |
67 | {username}
68 |
69 |
70 |
71 | [
72 | {roles.map((role, i, Roles) =>
73 | i + 1 === Roles.length ? (
74 | {role}
75 | ) : (
76 | {role} ,
77 | )
78 | )}
79 | ]
80 |
81 |
82 | {image.preview ? (
83 |

90 | ) : (
91 |
99 | )}
100 |
101 |
133 |
134 |
135 | );
136 | }
137 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MERN-auth-roles
2 |
3 | [](https://www.codacy.com/gh/adelpro/MERN-auth-roles-boilerplate/dashboard?utm_source=github.com&utm_medium=referral&utm_content=adelpro/MERN-auth-roles-boilerplate&utm_campaign=Badge_Grade)
4 | 
5 |
6 | ## Live website
7 |
8 | https://mern-auth-roles.onrender.com
9 |
10 | ## About
11 |
12 | MERN-auth-roles a full-stack MERN (MongoDB Express React Node) boilerplate starter application with React, Recoil, authentication, roles, JWT, protected api
13 | 
14 |
15 | We have two parts in the application
16 |
17 | ## MERN-AUTH-ROLES-Backend (sever)
18 |
19 | ### Futures
20 |
21 | ✓ User with roles (Admin, Manager, user)
22 |
23 | ✓ NodeJS server
24 |
25 | ✓ Token and refresh token
26 |
27 | ✓ async/await syntax
28 |
29 | ✓ Server side validation
30 |
31 | ✓ .env file configuration
32 |
33 | ✓ Profile image upload with Multer (delete old image and replace it with the new image),
34 |
35 | ✓ Cross-origin resource sharing (CORS)
36 |
37 | ✓ Limit repeated requests such as password reset.
38 |
39 | ✓ Cookies
40 |
41 | ✓ Password Hashing
42 |
43 | ✓ Real-time notifications using Socket.io
44 |
45 | run this command
46 |
47 | ```
48 | cd backend
49 | npm install
50 | npm run dev
51 | ```
52 |
53 | ## MERN-AUTH-ROLES-Frontend (client)
54 |
55 | ### Futures
56 |
57 | ✓ React DevTools desabled in production
58 |
59 | ✓ Axios with Interceptors to manage fetchs
60 |
61 | ✓ Token persist only in memory and cookies
62 |
63 | ✓ Refresh Token (in memory) and access token (in cookies)
64 |
65 | ✓ Hookform: to manage form inputs
66 |
67 | ✓ YUP: to validate inputs
68 |
69 | ✓ react-multi-select-component
70 |
71 | ✓ Recoil and Recoil-persist: to manage states
72 |
73 | ✓ react-icons and @uiball/loaders to give a nice look to the UI
74 |
75 | ✓ Protected routes with Higher order components
76 |
77 | ✓ Layout component, it will be very easy to navigation and footer
78 |
79 | ✓ Profile image upload
80 |
81 | ✓ Real-time notifications using Socket.io client
82 |
83 | ### Run the code
84 |
85 | you can run the code by executing this command
86 |
87 | ```
88 | cd frontend
89 | npm install
90 | npm start
91 | ```
92 |
93 | ## Screenshots
94 |
95 | ### Home page
96 |
97 | 
98 |
99 | ### Dash
100 |
101 | 
102 |
103 | ### Users
104 |
105 | 
106 | 
107 |
108 | ### Notes
109 |
110 | 
111 |
112 | 
113 |
114 | ### Notifications
115 |
116 | [MERN-auth-roles](https://i.imgur.com/yk2nrWy.png)
117 |
118 | ### Youtube Video demonstration
119 |
120 |
125 |
126 |
127 | ## Contact us 📨
128 |
129 | [![twitter][1.1]][1]
130 | [![facebook][2.1]][2]
131 | [![github][3.1]][3]
132 | [![medium][4.1]][4]
133 |
134 | ## Support me ❤️
135 |
136 | [](https://ko-fi.com/adelbenyahia)
137 | [](https://www.buymeacoffee.com/Adel.benyahia/)
138 | [](https://www.paypal.com/paypalme/adelbenyahia)
139 | [](bitcoin:1PstR1HYTG8FbVRR7YZhQftYumVAURXuq7?label=Quranipfs&message=Payment%20to%20Quranipfs)
140 |
141 | [1]: https://www.twitter.com/adelpro
142 | [1.1]: http://i.imgur.com/tXSoThF.png "twitter icon with padding"
143 | [2]: https://www.facebook.com/adel.benyahia
144 | [2.1]: http://i.imgur.com/P3YfQoD.png "facebook icon with padding"
145 | [3]: https://github.com/adelpro
146 | [3.1]: http://i.imgur.com/0o48UoR.png "github icon with padding"
147 | [4]: adelpro.medium.com
148 | [4.1]: https://i.imgur.com/tijdQEw.png "medium icon with padding"
149 |
150 | ## Thank you
151 |
152 | [](https://github.com/adelpro/MERN-auth-roles-boilerplate/stargazers)
153 |
154 | [](https://github.com/adelpro/MERN-auth-roles-boilerplate/network/members)
155 |
--------------------------------------------------------------------------------
/frontend/src/components/notes/EditNoteForm.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 | import { useForm } from 'react-hook-form';
3 | import { useRecoilState } from 'recoil';
4 | import { yupResolver } from '@hookform/resolvers/yup';
5 | import * as yup from 'yup';
6 | import { useParams } from 'react-router-dom';
7 | import { MdAutorenew, MdEditNote, MdSystemUpdateAlt } from 'react-icons/md';
8 | import styles from '../../App.module.css';
9 | import { AccessToken } from '../../recoil/atom';
10 | import useAxiosPrivate from '../../hooks/useAxiosPrivate';
11 |
12 | export default function EditUserForm() {
13 | const axiosPrivate = useAxiosPrivate();
14 | const [accessToken] = useRecoilState(AccessToken);
15 | const messageRef = useRef();
16 | const [isloading, setIsloading] = useState(false);
17 | const [message, setMessage] = useState(null);
18 | const { id } = useParams();
19 | const schema = yup.object().shape({
20 | title: yup.string().min(4).required('Title is required'),
21 | text: yup.string().min(10).required('Text is required'),
22 | completed: yup.boolean(),
23 | user: yup.string(),
24 | id: yup.string()
25 | });
26 | const {
27 | register,
28 | handleSubmit,
29 | reset,
30 | formState: { errors }
31 | } = useForm({
32 | resolver: yupResolver(schema)
33 | });
34 |
35 | // Fetching default note data with id:at component mount
36 | useEffect(() => {
37 | setIsloading(true);
38 | const controller = new AbortController();
39 | const getNote = async () => {
40 | try {
41 | const result = await axiosPrivate.post(
42 | '/notes/one',
43 | { id },
44 | {
45 | signal: controller.signal
46 | }
47 | );
48 | const { _id, user, title, text, completed } = result.data;
49 | reset({ id: _id, user, title, text, completed });
50 | setMessage(null);
51 | } catch (err) {
52 | setMessage(err?.response?.message);
53 | } finally {
54 | setIsloading(false);
55 | }
56 | };
57 | if (accessToken) getNote();
58 | return () => {
59 | controller?.abort();
60 | };
61 | }, [axiosPrivate, accessToken, id, reset]);
62 |
63 | const onSubmit = async (data) => {
64 | setIsloading(true);
65 | setMessage(null);
66 | await axiosPrivate
67 | .patch('/notes', data)
68 |
69 | .then((result) => {
70 | setIsloading(false);
71 | setMessage(result?.response?.message);
72 | })
73 | .catch((err) => {
74 | if (!err?.response?.status) {
75 | setMessage(err?.response?.statusText ? err?.response?.statusText : 'No server response');
76 | } else if (err.status === 409) {
77 | setMessage('Username exist already');
78 | } else if (err?.response?.status === 401) {
79 | setMessage('Unauthorized');
80 | } else {
81 | setMessage(err?.response?.statusText);
82 | }
83 | setIsloading(false);
84 | });
85 | };
86 |
87 | if (isloading) return Loading ...
;
88 | return (
89 |
90 |
91 |
92 |
Update Note
93 |
94 |
130 |
131 | );
132 | }
133 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/Login.js:
--------------------------------------------------------------------------------
1 | import { useRef, useState, useEffect } from 'react';
2 | import { useLocation, useNavigate } from 'react-router-dom';
3 | import { Ring } from '@uiball/loaders';
4 | import { useRecoilState, useSetRecoilState } from 'recoil';
5 | import { MdLogin, MdPassword, MdRemoveRedEye } from 'react-icons/md';
6 | import { AccessToken, Persist } from '../../recoil/atom';
7 | import styles from '../../App.module.css';
8 | import axios from '../../api/axios';
9 |
10 | export default function Login() {
11 | const setAccessToken = useSetRecoilState(AccessToken);
12 | const [persist, setPersist] = useRecoilState(Persist);
13 | const [username, setUsername] = useState('');
14 | const [password, setPassword] = useState('');
15 | const [isloading, setIsloading] = useState(false);
16 | const [error, setError] = useState(null);
17 | const usernameRef = useRef();
18 | const [passwordType, setPasswordType] = useState(true);
19 | const errorRef = useRef();
20 | const location = useLocation();
21 | const navigate = useNavigate();
22 | const handleSubmit = async (e) => {
23 | e.preventDefault();
24 | setIsloading(true);
25 | setError(null);
26 | try {
27 | const result = await axios.post('/auth', { username, password }, { withCredentials: true });
28 | if (result?.data?.accessToken) {
29 | setAccessToken(result?.data?.accessToken);
30 | }
31 | setIsloading(false);
32 | setError(null);
33 | setUsername('');
34 | setPassword('');
35 | navigate(location.state?.from?.pathname || '/dash', {
36 | replace: true
37 | });
38 | } catch (err) {
39 | if (!err?.response?.status) {
40 | setError('No server response');
41 | } else if (err?.response?.status === 400) {
42 | setError('Missing username or password');
43 | } else if (err?.response?.status === 401) {
44 | setError('Unauthorized');
45 | } else {
46 | setError(err?.message);
47 | }
48 | setIsloading(false);
49 | errorRef.current.focus();
50 | }
51 | };
52 | useEffect(() => usernameRef.current.focus(), []);
53 | useEffect(() => setError(null), [username, password]);
54 | return (
55 |
56 |
62 |
63 |
Login
64 |
65 |
144 |
145 | );
146 | }
147 |
--------------------------------------------------------------------------------
/backend/controllers/userController.js:
--------------------------------------------------------------------------------
1 | const user = require('../models/user')
2 | const note = require('../models/note')
3 | const bcrypt = require('bcrypt')
4 | const fs = require('fs')
5 | const path = require('path')
6 | const notification = require('../models/notification')
7 | const validator = require('validator');
8 |
9 | // @desc Get all users
10 | // @Route GET /users
11 | // @Access Private
12 | const getAllUsers = async (req, res) => {
13 | const users = await user.find().select('-password').lean().exec()
14 | if (!users) {
15 | return res.status(400).json({ message: 'No users found' })
16 | }
17 | res.json(users)
18 | }
19 | // @desc Get one user by ID
20 | // @Route POST /users/one
21 | // @Access Private
22 | const getOneUser = async (req, res) => {
23 | const { id } = req.body
24 | if (!id) {
25 | return res
26 | .status(400)
27 | .json({ message: 'Verify your data and proceed again r35476' })
28 | }
29 | // Check if the note exist
30 | const oneUser = await user.findById(id).lean().exec()
31 | if (!oneUser) {
32 | return res
33 | .status(400)
34 | .json({ message: `Can't find a user with this id: ${id}` })
35 | }
36 | res.json(oneUser)
37 | }
38 |
39 | // @desc Create new user
40 | // @Route POST /users
41 | // @Access Private
42 | const createNewUser = async (req, res) => {
43 | const { username, password, email, roles } = req.body
44 | //Confirm data
45 | if (
46 | !username ||
47 | username.length < 4 ||
48 | !password ||
49 | password.length < 6 ||
50 | !email ||
51 | !Array.isArray(roles) ||
52 | !roles.length
53 | ) {
54 | return res
55 | .status(400)
56 | .json({ message: 'Verify your data and proceed again' })
57 | }
58 |
59 | //check for email validity
60 | if (!validator.isEmail(email)) {
61 | return res.status(400).json({ message: 'please enter a valid email address'})
62 | }
63 |
64 |
65 | // Check for duplicate
66 | const duplicate = await user.findOne({ username }).lean().exec()
67 | if (duplicate) {
68 | return res.status(409).json({ message: 'user already exist' })
69 | }
70 | const hashedPassword = await bcrypt.hash(password, 10)
71 | //create new user
72 | const newUser = await user.create({
73 | username,
74 | password: hashedPassword,
75 | email,
76 | roles,
77 | })
78 | if (newUser) {
79 | res.json({ message: `new user ${username} created with success` })
80 | } else {
81 | res.status(400).json({
82 | message: 'user creation failed, please verify your data and try again',
83 | })
84 | }
85 | }
86 |
87 | // @desc Update a user
88 | // @Route PATCH /users
89 | // @Private access
90 | const updateUser = async (req, res) => {
91 | const { id, username, password, roles, active } = req.body
92 | //Confirm data
93 | if (
94 | !username ||
95 | !Array.isArray(roles) ||
96 | !roles.length ||
97 | typeof active !== 'boolean'
98 | ) {
99 | return res
100 | .status(400)
101 | .json({ message: 'Verify your data and proceed again r98451' })
102 | }
103 | // Check for duplicate
104 | const updateUser = await user.findById(id).exec()
105 | if (!updateUser) {
106 | return res
107 | .status(400)
108 | .json({ message: `Can't find a user with this id: ${id}` })
109 | }
110 | // Check for duplicate
111 | const duplicate = await user.findOne({ username }).lean().exec()
112 | //Allow update to the original user
113 | if (duplicate && duplicate?._id.toString() !== id) {
114 | return res.status(409).json({
115 | message: `user with the same username: ${username} exists already`,
116 | })
117 | }
118 | updateUser.username = username
119 | updateUser.roles = roles
120 | updateUser.active = active
121 | if (password) {
122 | updateUser.password = await bcrypt.hash(password, 10)
123 | }
124 | await updateUser.save()
125 | res.json({ message: `User: ${username} updated with success` })
126 | }
127 |
128 | // @desc delete a user
129 | // @Route DELETE /users
130 | // @Private access
131 | const deleteUser = async (req, res) => {
132 | const { id } = req.body
133 | if (!id) {
134 | return res
135 | .status(400)
136 | .json({ message: `Can't find a user with this id: ${id}` })
137 | }
138 | const notes = await note.findOne({ user: id }).lean().exec()
139 | if (notes?.length) {
140 | return res
141 | .status(400)
142 | .json({ message: `User with id: ${id} has assigned notes` })
143 | }
144 | const deleteUser = await user.findById(id).exec()
145 | if (!deleteUser) {
146 | return res.status(400).json({ message: `Can't find a user with id: ${id}` })
147 | }
148 | const result = await deleteUser.deleteOne()
149 | if (!result) {
150 | return res
151 | .status(400)
152 | .json({ message: `Can't delete the user with id: ${id}` })
153 | }
154 | res.json({ message: `User with id: ${id} deleted with success` })
155 | }
156 |
157 | // @desc Update a user image
158 | // @Route POST /users
159 | // @Private access
160 | const updateUserImage = async (req, res) => {
161 | const fileName = req.file.filename
162 | // Adding image to mongodb
163 | const { id } = req.body
164 | if (!id) {
165 | return res
166 | .status(400)
167 | .json({ message: `Can't find a user with this id: ${id}` })
168 | }
169 | const updateUser = await user.findById(id).exec()
170 | if (updateUser?.length) {
171 | return res
172 | .status(400)
173 | .json({ message: `Can't find a user with this id: ${id}` })
174 | }
175 | // Remove old photo
176 | if (updateUser.profileImage) {
177 | const oldPath = path.join(__dirname, '..', updateUser.profileImage)
178 | fs.access(oldPath, (err) => {
179 | if (err) {
180 | return
181 | }
182 | fs.rmSync(oldPath, {
183 | force: true,
184 | })
185 | })
186 | }
187 | // adding new photo to mongoDB
188 | updateUser.profileImage = '/images/' + fileName
189 | await updateUser.save()
190 |
191 | // add notification for updated profile image
192 | await notification.create({
193 | user: id,
194 | title: 'updated profile image',
195 | type: 1,
196 | text: `Profile image updated at ${new Date()}`,
197 | read: false,
198 | })
199 | res.json({ message: 'image uploaded wtih success' })
200 | }
201 | module.exports = {
202 | createNewUser,
203 | updateUser,
204 | getAllUsers,
205 | getOneUser,
206 | deleteUser,
207 | updateUserImage,
208 | }
209 |
--------------------------------------------------------------------------------
/frontend/src/components/users/NewUserFrom.js:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from 'react';
2 | import { Controller, useForm } from 'react-hook-form';
3 | import { yupResolver } from '@hookform/resolvers/yup';
4 | import * as yup from 'yup';
5 | import { MultiSelect } from 'react-multi-select-component';
6 | import {
7 | MdAdd,
8 | MdAutorenew,
9 | MdRemoveRedEye,
10 | MdPassword,
11 | MdSupervisorAccount
12 | } from 'react-icons/md';
13 | import { Ring } from '@uiball/loaders';
14 | import { useLocation, useNavigate } from 'react-router-dom';
15 | import ROLES from '../../config/roles';
16 | import styles from '../../App.module.css';
17 | import useAxiosPrivate from '../../hooks/useAxiosPrivate';
18 |
19 | export default function NewUserFrom() {
20 | const navigate = useNavigate();
21 | const location = useLocation();
22 | const axiosPrivate = useAxiosPrivate();
23 | const messageRef = useRef();
24 | const [passwordType, setPasswordType] = useState('type');
25 | const [isloading, setIsloading] = useState(false);
26 | const [message, setMessage] = useState(null);
27 | const schema = yup.object().shape({
28 | username: yup.string().min(4).required('Username is required'),
29 | email: yup.string().email('Invalid email').required('Email is required'),
30 | password: yup.string().min(6, 'Min 6 characters').required('Password is required'),
31 | // confirmationPassword: yup.string().oneOf([yupref("password"), null]),
32 | roles: yup
33 | .array()
34 | .min(1, 'Please select at least one role')
35 | .required('Required: Please select at least one role')
36 | });
37 | const {
38 | register,
39 | handleSubmit,
40 | reset,
41 | control,
42 | formState: { errors }
43 | } = useForm({
44 | resolver: yupResolver(schema)
45 | });
46 | const onSubmit = async (data) => {
47 | setIsloading(true);
48 | setMessage(null);
49 | const newRoles = data?.roles.map((element) => element.value);
50 | await axiosPrivate
51 | .post('/users', { ...data, roles: newRoles })
52 | .then((result) => {
53 | setIsloading(false);
54 | setMessage(result?.data?.message);
55 | navigate(location.state?.from?.pathname || '/dash/users', {
56 | replace: true
57 | });
58 | })
59 | .catch((err) => {
60 | if (!err?.response?.status) {
61 | setMessage(err?.response?.statusText ? err?.response?.statusText : 'No server response');
62 | } else if (err?.response?.status === 409) {
63 | setMessage('Username exist already');
64 | } else if (err.status === 401) {
65 | setMessage('Unauthorized');
66 | } else {
67 | setMessage(err?.response?.statusText);
68 | }
69 | setIsloading(false);
70 | });
71 | };
72 |
73 | if (isloading) return Loading ...
;
74 | return (
75 |
76 |
77 |
78 |
Singup
79 |
80 |
172 |
173 | );
174 | }
175 |
--------------------------------------------------------------------------------
/frontend/src/components/notifications/NotificationsList.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 | import { MdArrowForwardIos, MdDeleteOutline, MdDeleteSweep, MdDoneAll } from 'react-icons/md';
3 | import { useRecoilValue } from 'recoil';
4 | import styles from '../../App.module.css';
5 | import useAxiosPrivate from '../../hooks/useAxiosPrivate';
6 | import { AccessToken, NotificationsLength } from '../../recoil/atom';
7 | import useAuth from '../../hooks/useAuth';
8 |
9 | export default function NotificationsList() {
10 | const { id } = useAuth();
11 | const [limit] = useState(3);
12 | const [page, setPage] = useState(0);
13 | const accessToken = useRecoilValue(AccessToken);
14 | const notificationsLength = useRecoilValue(NotificationsLength);
15 | const axiosPrivate = useAxiosPrivate();
16 | const [data, setData] = useState(null);
17 | const [isLoading, setIsloading] = useState(false);
18 | const [error, setError] = useState(null);
19 | const [message, setMessage] = useState(null);
20 | const messageRef = useRef();
21 | useEffect(() => {
22 | setIsloading(true);
23 | const controller = new AbortController();
24 | const getNotifications = async () => {
25 | try {
26 | const result = await axiosPrivate.post(
27 | '/notifications',
28 | {
29 | id,
30 | limit,
31 | page
32 | },
33 | {
34 | signal: controller.signal
35 | }
36 | );
37 | setData(result?.data);
38 | setError(null);
39 | } catch (err) {
40 | setData(null);
41 | setError(err?.response?.message);
42 | } finally {
43 | setIsloading(false);
44 | }
45 | };
46 | if (accessToken) getNotifications();
47 | return () => {
48 | controller?.abort();
49 | };
50 | }, [axiosPrivate, accessToken]);
51 | const handleMarkAllAsRead = async () => {
52 | try {
53 | const result = await axiosPrivate.patch('/notifications/all', { id });
54 | const newData = data;
55 | newData.notifications.forEach((element) => {
56 | const obj = element;
57 | obj.read = true;
58 | return obj;
59 | });
60 | setData(newData);
61 | setMessage(result?.data?.message);
62 | messageRef.current.focus();
63 | } catch (err) {
64 | setMessage(err?.response?.data?.message);
65 | messageRef.current.focus();
66 | }
67 | };
68 | const handleDeleteNotification = async (notificationId) => {
69 | try {
70 | const result = await axiosPrivate.delete('/notifications', { data: { id: notificationId } });
71 | const newData = data?.notifications?.filter((item) => item._id !== notificationId);
72 | console.log(newData);
73 | setData(newData);
74 | setMessage(result?.data?.message);
75 | messageRef.current.focus();
76 | } catch (err) {
77 | setMessage(err?.response?.data?.message);
78 | messageRef.current.focus();
79 | }
80 | };
81 | const handleDeleteAll = async () => {
82 | try {
83 | const result = await axiosPrivate.delete('/notifications/all', { data: { id } });
84 | setData();
85 | setMessage(result?.data?.message);
86 | messageRef.current.focus();
87 | } catch (err) {
88 | setMessage(err?.response?.data?.message);
89 | messageRef.current.focus();
90 | }
91 | };
92 | if (error) return {error.message}
;
93 | if (isLoading) return Loading...
;
94 | let content = (
95 | <>
96 | Notifications list
97 | No data to show
98 | >
99 | );
100 |
101 | if (data?.notifications?.length > 0) {
102 | content = (
103 | <>
104 | Notifications list
105 |
106 | {data?.notifications?.map((notification) => {
107 | return (
108 |
109 |
-
110 |
111 |
112 |
113 | {notification.title}
114 |
115 |
116 |
{notification.text}
117 | {notification.read ?
✅ read
:
❌ Not read
}
118 |
119 |
120 |
127 |
128 |
129 |
130 | );
131 | })}
132 |
133 | >
134 | );
135 | }
136 | // Render data
137 | return (
138 | <>
139 | {content}
140 |
141 |
152 |
163 |
164 |
165 |
173 |
174 | page: {page + 1} / {data?.totalpage}
175 |
176 |
184 |
185 |
186 | {message?.message}
187 |
188 | >
189 | );
190 | }
191 |
--------------------------------------------------------------------------------
/frontend/src/components/users/EditUserForm.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 | import { Controller, useForm } from 'react-hook-form';
3 | import { useRecoilState } from 'recoil';
4 | import { yupResolver } from '@hookform/resolvers/yup';
5 | import * as yup from 'yup';
6 | import { useParams } from 'react-router-dom';
7 | import {
8 | MdAutorenew,
9 | MdEditNote,
10 | MdPassword,
11 | MdRemoveRedEye,
12 | MdSystemUpdateAlt
13 | } from 'react-icons/md';
14 | import { MultiSelect } from 'react-multi-select-component';
15 | import styles from '../../App.module.css';
16 | import useLocalStorage from '../../hooks/useLocalStorage';
17 | import { AccessToken } from '../../recoil/atom';
18 | import ROLES from '../../config/roles';
19 | import useAxiosPrivate from '../../hooks/useAxiosPrivate';
20 |
21 | export default function EditUserForm() {
22 | const [accessToken, setAccessToken] = useRecoilState(AccessToken);
23 | const [persist] = useLocalStorage('persist', false);
24 | const messageRef = useRef();
25 | const [isloading, setIsloading] = useState(false);
26 | const [message, setMessage] = useState(null);
27 | const { id } = useParams();
28 | const [passwordType, setPasswordType] = useState(true);
29 | const axiosPrivate = useAxiosPrivate();
30 | const schema = yup.object().shape({
31 | username: yup.string().min(4).required('Username is required'),
32 | email: yup.string().email('Invalid email').required('Email is required'),
33 | active: yup.boolean(),
34 | // password: yup.string().min(6, "Min 6 characters"),
35 | // .required("Password is required"),
36 | // confirmationPassword: yup.string().oneOf([yupref("password"), null]),
37 | roles: yup
38 | .array()
39 | .min(1, 'Please select at least one role')
40 | .required('Required: Please select at least one role')
41 | });
42 | const {
43 | register,
44 | handleSubmit,
45 | reset,
46 | control,
47 | formState: { errors }
48 | } = useForm({
49 | // shouldUseNativeValidation: true,
50 | resolver: yupResolver(schema)
51 | });
52 | // fetching default user data with id:
53 | useEffect(() => {
54 | setIsloading(true);
55 | const controller = new AbortController();
56 | const getUser = async () => {
57 | try {
58 | const result = await axiosPrivate.post(
59 | '/users/one',
60 | { id },
61 | {
62 | signal: controller.signal
63 | }
64 | );
65 | const { username, email, roles, active } = result.data;
66 | const newRoles = roles.map((element) => {
67 | return { label: element, value: element };
68 | });
69 | reset({ username, email, roles: newRoles, active });
70 | setMessage(null);
71 | } catch (err) {
72 | setMessage(err?.response?.message);
73 | } finally {
74 | setIsloading(false);
75 | }
76 | };
77 | if (accessToken) getUser();
78 | return () => {
79 | controller?.abort();
80 | };
81 | }, [accessToken, axiosPrivate, id, persist, reset, setAccessToken]);
82 |
83 | const onSubmit = async (data) => {
84 | setIsloading(true);
85 | setMessage(null);
86 | const { username, password, email, roles, active } = data;
87 | const newRoles = roles.map((element) => element.value);
88 | let body = null;
89 | if (password) {
90 | body = { ...data, roles: newRoles, id };
91 | } else {
92 | body = { id, username, email, roles: newRoles, active };
93 | }
94 | await axiosPrivate
95 | .patch('/users', body)
96 | .then((result) => {
97 | setIsloading(false);
98 | setMessage(result?.response?.message);
99 | })
100 | .catch((err) => {
101 | if (!err?.response?.status) {
102 | setMessage(err?.response?.statusText ? err?.response?.statusText : 'No server response');
103 | } else if (err?.response?.status === 409) {
104 | setMessage('Username exist already');
105 | } else if (err?.response?.status === 401) {
106 | setMessage('Unauthorized');
107 | } else {
108 | setMessage(err?.response?.statusText);
109 | }
110 | setIsloading(false);
111 | });
112 | };
113 |
114 | if (isloading) return Loading ...
;
115 | return (
116 |
117 |
118 |
119 |
Update user
120 |
121 |
212 |
213 | );
214 | }
215 |
--------------------------------------------------------------------------------