├── client
├── src
│ ├── App.css
│ ├── components
│ │ ├── Users
│ │ │ ├── Users.module.scss
│ │ │ ├── User
│ │ │ │ ├── User.module.scss
│ │ │ │ └── User.js
│ │ │ └── Users.js
│ │ ├── Home
│ │ │ ├── Home.module.scss
│ │ │ └── Home.js
│ │ ├── Layout
│ │ │ └── Layout.js
│ │ ├── SplashScreen
│ │ │ ├── SplashScreen.js
│ │ │ └── SplashScreen.module.scss
│ │ ├── Login
│ │ │ ├── Login.module.scss
│ │ │ └── Login.js
│ │ ├── Signup
│ │ │ ├── Signup.module.scss
│ │ │ └── Signup.js
│ │ └── Navbar
│ │ │ ├── Navbar.module.scss
│ │ │ └── Navbar.js
│ ├── utils
│ │ └── utils.js
│ ├── setupTests.js
│ ├── App.test.js
│ ├── index.css
│ ├── reportWebVitals.js
│ ├── index.js
│ ├── logo.svg
│ ├── contexts
│ │ └── auth-context.js
│ └── App.js
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── index.html
├── .gitignore
├── package.json
└── README.md
├── .gitignore
├── server
├── routes
│ ├── users.js
│ └── auth.js
├── package.json
├── data
│ └── data.js
├── utils
│ └── auth.js
├── .env
├── controllers
│ ├── users.js
│ └── auth.js
├── app.js
├── middlewares
│ └── auth.js
└── package-lock.json
└── README.md
/client/src/App.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SukhjinderArora/refresh-token-auth-app/HEAD/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SukhjinderArora/refresh-token-auth-app/HEAD/client/public/logo192.png
--------------------------------------------------------------------------------
/client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SukhjinderArora/refresh-token-auth-app/HEAD/client/public/logo512.png
--------------------------------------------------------------------------------
/client/src/components/Users/Users.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | margin-top: 150px;
3 | }
4 |
5 | .userContainer {
6 | margin-bottom: 20px;
7 | }
--------------------------------------------------------------------------------
/client/src/utils/utils.js:
--------------------------------------------------------------------------------
1 | const STATUS = Object.freeze({
2 | IDLE: 'idle',
3 | PENDING: 'pending',
4 | SUCCEEDED: 'succeeded',
5 | FAILED: 'failed',
6 | });
7 |
8 | export {
9 | STATUS
10 | }
--------------------------------------------------------------------------------
/client/src/components/Home/Home.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | margin-top: 120px;
3 | padding: 15px;
4 | }
5 |
6 | .heading {
7 | text-align: center;
8 | }
9 |
10 | .colorTeal {
11 | color: teal;
12 | }
13 |
14 | .colorBlack {
15 | color: #1b2839;
16 | }
--------------------------------------------------------------------------------
/client/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/client/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render( );
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | *::before,
2 | *,
3 | *::after {
4 | margin: 0;
5 | padding: 0;
6 | box-sizing: border-box;
7 | font-family: inherit;
8 | }
9 |
10 | body {
11 | margin: 0;
12 | font-family: 'Urbanist', sans-serif;
13 | -webkit-font-smoothing: antialiased;
14 | -moz-osx-font-smoothing: grayscale;
15 | }
--------------------------------------------------------------------------------
/client/src/components/Layout/Layout.js:
--------------------------------------------------------------------------------
1 | import { Outlet } from "react-router-dom";
2 |
3 | import Navbar from "../Navbar/Navbar";
4 |
5 | const Layout = () => {
6 | return (
7 |
13 | );
14 | };
15 |
16 | export default Layout;
--------------------------------------------------------------------------------
/client/src/components/SplashScreen/SplashScreen.js:
--------------------------------------------------------------------------------
1 | import styles from './SplashScreen.module.scss';
2 |
3 | const SplashScreen = () => {
4 | return (
5 |
6 |
7 |
8 | ...loading
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | export default SplashScreen;
--------------------------------------------------------------------------------
/client/src/components/SplashScreen/SplashScreen.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | background-color: #fff;
3 | color: teal;
4 | position: fixed;
5 | top: 0;
6 | left: 0;
7 | height: 100%;
8 | width: 100%;
9 | z-index: 999;
10 | }
11 |
12 | .iconContainer {
13 | position: absolute;
14 | top: 50%;
15 | left: 50%;
16 | translate: (-50%, -50%);
17 | color: teal;
18 | }
--------------------------------------------------------------------------------
/client/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/server/routes/users.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router();
2 |
3 | const { isAuthenticated } = require('../middlewares/auth');
4 |
5 | const usersController = require('../controllers/users');
6 |
7 | router.get('/list', isAuthenticated, usersController.getUsersList);
8 |
9 | router.get('/me', isAuthenticated, usersController.getAuthenticatedUser);
10 |
11 | router.get('/:id', isAuthenticated, usersController.getUserById);
12 |
13 | module.exports = router;
--------------------------------------------------------------------------------
/client/src/components/Home/Home.js:
--------------------------------------------------------------------------------
1 | import { useAuth } from "../../contexts/auth-context";
2 |
3 | import styles from "./Home.module.scss";
4 |
5 | const Home = () => {
6 | const { user } = useAuth();
7 |
8 | return (
9 |
10 |
Welcome {user.name}
11 |
12 | );
13 | };
14 |
15 | export default Home;
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node app.js"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "cookie-parser": "^1.4.6",
15 | "cors": "^2.8.5",
16 | "dotenv": "^16.0.3",
17 | "express": "^4.18.2",
18 | "http-errors": "^2.0.0",
19 | "jsonwebtoken": "^9.0.0",
20 | "ms": "^2.1.3"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/components/Users/User/User.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | max-width: 600px;
4 | margin: 0 auto;
5 | gap: 20px;
6 | align-items: center;
7 | box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2);
8 | padding: 20px 18px;
9 | }
10 |
11 | .imageContainer {
12 | width: 200px;
13 | height: 100px;
14 | overflow: hidden;
15 | flex-basis: 100px;
16 | flex-shrink: 0;
17 | flex-grow: 0;
18 | border-radius: 50%;
19 | }
20 |
21 | .image {
22 | width: 100%;
23 | height: 100%;
24 | object-fit: cover;
25 | }
--------------------------------------------------------------------------------
/server/data/data.js:
--------------------------------------------------------------------------------
1 | // data.js
2 | const users = [
3 | {
4 | id: 1,
5 | name: "John Doe",
6 | email: "johndoe@example.com",
7 | userName: "johndoe",
8 | password: "JohnDoe@123"
9 | },
10 | {
11 | id: 2,
12 | name: "Jane Smith",
13 | email: "janesmith@example.com",
14 | userName: "janesmith",
15 | password: "JaneSmith@123"
16 | },
17 | ];
18 |
19 | const tokens = []; // [{userId: number, refreshToken: string, expirationTime: number }]
20 |
21 | module.exports = { users, tokens };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React and NodeJS Authentication with Refresh & Access Tokens: A Step-by-Step Guide
2 |
3 | ### Learn How to implement refresh and access token based authentication in React and Node.js
4 |
5 | ## Development Workflow
6 |
7 | Run the following commands to run this application locally on your system:
8 |
9 | ```
10 | cd server
11 | npm install
12 | npm start
13 | ```
14 |
15 | ```
16 | cd client
17 | npm install
18 | npm start
19 | ```
20 |
21 | You can read the full article at: https://whatisweb.dev/react-and-nodejs-authentication-with-refresh-access-tokens-a-step-by-step-guide
22 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/server/routes/auth.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router();
2 |
3 | const authController = require('../controllers/auth');
4 | const authMiddleware = require('../middlewares/auth');
5 |
6 | router.post(
7 | '/sign-up',
8 | authController.signUp,
9 | authMiddleware.generateAuthTokens
10 | );
11 |
12 | router.post(
13 | '/login',
14 | authController.login,
15 | authMiddleware.generateAuthTokens
16 | );
17 |
18 | router.post(
19 | '/logout',
20 | authMiddleware.isAuthenticated,
21 | authController.logout
22 | );
23 |
24 | router.post(
25 | '/refresh',
26 | authController.refreshAccessToken
27 | );
28 |
29 | module.exports = router;
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 | import { AuthProvider } from './contexts/auth-context';
7 |
8 | const root = ReactDOM.createRoot(document.getElementById('root'));
9 | root.render(
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | // If you want to start measuring performance in your app, pass a function
18 | // to log results (for example: reportWebVitals(console.log))
19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
20 | reportWebVitals();
--------------------------------------------------------------------------------
/client/src/components/Users/User/User.js:
--------------------------------------------------------------------------------
1 | import styles from './User.module.scss';
2 |
3 | const User = ({ user }) => {
4 | return (
5 |
6 |
7 |
12 |
13 |
14 |
{user.name}
15 |
16 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer
17 | ornare neque quis purus tempus interdum. Lorem ipsum dolor sit amet,
18 | consectetur adipiscing elit.{" "}
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default User;
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
25 |
26 | # dependencies
27 | /node_modules
28 | /.pnp
29 | .pnp.js
30 |
31 | # testing
32 | /coverage
33 |
34 | # production
35 | /build
36 |
37 | # misc
38 | .DS_Store
39 | .env.local
40 | .env.development.local
41 | .env.test.local
42 | .env.production.local
43 |
44 | npm-debug.log*
45 | yarn-debug.log*
46 | yarn-error.log*
47 |
--------------------------------------------------------------------------------
/server/utils/auth.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 |
3 | const { tokens } = require('../data/data');
4 |
5 | const dev = process.env.NODE_ENV === 'development';
6 |
7 | const generateJWT = (userId, secret, expirationTime) => {
8 | return jwt.sign(
9 | {
10 | userId,
11 | },
12 | secret,
13 | { expiresIn: expirationTime }
14 | );
15 | }
16 | const clearTokens = async (req, res) => {
17 | const { signedCookies = {} } = req;
18 | const { refreshToken } = signedCookies;
19 | if (refreshToken) {
20 | const index = tokens.findIndex(token => token.refreshToken === refreshToken);
21 | if(index) {
22 | tokens.splice(index, 1);
23 | }
24 | }
25 | res.clearCookie('refreshToken', {
26 | httpOnly: true,
27 | secure: !dev,
28 | signed: true,
29 | });
30 | };
31 |
32 | module.exports = {
33 | generateJWT,
34 | clearTokens
35 | };
--------------------------------------------------------------------------------
/client/src/components/Users/Users.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import axios from "axios";
3 |
4 | import User from "./User/User";
5 |
6 | import { useAuth } from "../../contexts/auth-context";
7 |
8 | import styles from './Users.module.scss';
9 |
10 | const Users = () => {
11 | const { token } = useAuth();
12 | const [users, setUsers] = useState([]);
13 |
14 | useEffect(() => {
15 | axios
16 | .get("/api/users/list", {
17 | headers: {
18 | Authorization: `Bearer ${token}`,
19 | },
20 | })
21 | .then((res) => {
22 | setUsers(res.data.data);
23 | })
24 | .catch((error) => {
25 | console.log("Something went wrong.", error);
26 | });
27 | }, [token]);
28 |
29 | return (
30 |
31 | {users.map((user) => (
32 |
33 |
34 |
35 | ))}
36 |
37 | );
38 | };
39 |
40 | export default Users;
--------------------------------------------------------------------------------
/server/.env:
--------------------------------------------------------------------------------
1 | PORT=5000
2 |
3 | ACCESS_TOKEN_SECRET=7rG7v5ElkhMpIHdQfs5l4sC+zprSYD2DNII4fzRusLevT2n0fEvpFzd6Ei2GpXzwEkghDxWxONRx0eCvcrsziY6EuF6GutZX+niTT6QJylTba/ydgURY9+7k1rn8w7sfiCAQPBg7c/SlY/nMRsDF4/5MSQATlfuSXX+9BIKgDmFWwZA19QqGS4cWKNiQO7JEhcNjkpy0FtaeUzK1/q0pG5Rjq8V8L8zbyhttUbAWd3h8N+m5vV7gi22HBrLlqpbFL0IIeb3GHWEe9z1nymyQNjLdxO6kcNRBNmWR7nRbamje6TJ6aHChebONL5h3GRWAFLwS188L41iNp67EqcNSqg==
4 |
5 | REFRESH_TOKEN_SECRET=LbIiOVV6MuKQ7A2KnGi6uW6vxrypJrouog48VY4bJjrJJdBbq0XLuKBU4Ia/Pzphvk4j6iUa7EEFnpgCBRewvxPCIyHZpHrGRZjUtCmbjGpLqIe5tlgMlEOPTzrwYAkgAHBNN6UzeZl55wlzOSiCWbhqcw2V6qDy8KYh+llIm/eBUVVlThNw7TDsn0LtcLBjhkzaBQqCUzZQmOLtTpCerjnzaWzlS2vSyP96zJ/yemlkgF21EZkiKPdoNrJPeaXNk4kqECHNlZ4mccpCTSWPr+RPR/vjGltCRL6nhJ1w6MqBDYFpXcAHcv54fz1bXcEwkhEO5imzoKa6aMg/1LPpTw==
6 |
7 | COOKIE_SECRET=Ak1jjwP38UQ3TUPatxFva2tytaYx0HnKxkfytoQAoignerppxxg7ogh2tUxnKhSe0JXVL7kbAZsHcnCFE3hY3OI2nuydrR9JL/xrj30EFBSQNjQ7FK8rY8S0QES/5z28k+etbBd9u7ms/bo/+YuA2ueJ2MiFCeRNH7UGknueF3JHEa+sfSVsf3QLIBgXd2WwmemRNYeqtpRmdxY29t8HeDwJqtoY9WdLU2onahQCzyuzD2/5aJWwwSIGyL7VeHSg7BQ/DDK+s2tv/IP6LVr3kVGMwhOGJksh6N5Ndeh9p22BkxsN4Nw1jzlxRGN4OhNmLdiPkFsAzj2B739z87mwNQ==
8 |
9 | ACCESS_TOKEN_LIFE=15m
10 |
11 | REFRESH_TOKEN_LIFE=30d
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.5",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "axios": "^1.4.0",
10 | "react": "^18.2.0",
11 | "react-dom": "^18.2.0",
12 | "react-hook-form": "^7.44.2",
13 | "react-router-dom": "^6.11.2",
14 | "react-scripts": "5.0.1",
15 | "web-vitals": "^2.1.4"
16 | },
17 | "scripts": {
18 | "start": "react-scripts start",
19 | "build": "react-scripts build",
20 | "test": "react-scripts test",
21 | "eject": "react-scripts eject"
22 | },
23 | "proxy": "http://localhost:5000",
24 | "eslintConfig": {
25 | "extends": [
26 | "react-app",
27 | "react-app/jest"
28 | ]
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">0.2%",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | },
42 | "devDependencies": {
43 | "sass": "^1.62.1"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/server/controllers/users.js:
--------------------------------------------------------------------------------
1 | const createError = require('http-errors');
2 |
3 | const { users } = require('../data/data');
4 |
5 | const getUsersList = async (req, res, next) => {
6 | const usersListWithOutPassword = users.map(user => {
7 | const {password, ...userWithOutPassword} = user;
8 | return {...userWithOutPassword};
9 | });
10 |
11 | return res.status(200).json({
12 | data: usersListWithOutPassword
13 | })
14 | };
15 |
16 | const getAuthenticatedUser = async (req, res, next) => {
17 | try {
18 | const { userId } = req;
19 |
20 | const authenticatedUser = users.find(user => user.id == userId);
21 |
22 | if(authenticatedUser) {
23 | return res.status(200).json({
24 | data: authenticatedUser
25 | })
26 | }
27 |
28 | const error = createError.NotFound();
29 | throw error;
30 |
31 | } catch(error) {
32 | return next(error);
33 | }
34 |
35 | };
36 |
37 | const getUserById = async (req, res, next) => {
38 | try {
39 | const { id } = req.params;
40 |
41 | const user = users.find(user => user.id == id);
42 |
43 | if (user) {
44 | return res.status(200).json({
45 | data: user
46 | })
47 | }
48 |
49 | const error = createError.NotFound();
50 | throw error;
51 | } catch(error) {
52 | return next(error);
53 | }
54 | };
55 |
56 | module.exports = {
57 | getUsersList,
58 | getAuthenticatedUser,
59 | getUserById
60 | }
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | require('dotenv').config();
3 |
4 | const express = require('express');
5 | const cors = require('cors');
6 | const cookieParser = require('cookie-parser');
7 |
8 | const authRoutes = require('./routes/auth');
9 | const usersRoutes = require('./routes/users');
10 |
11 | const { PORT, NODE_ENV } = process.env;
12 |
13 | const isDev = NODE_ENV === 'development';
14 |
15 | const app = express();
16 |
17 | if (isDev) {
18 | app.use(
19 | cors({
20 | origin: 'http://localhost:3000',
21 | optionsSuccessStatus: 200,
22 | credentials: true,
23 | })
24 | );
25 | }
26 |
27 | app.use(express.json({ type: 'application/json' }));
28 | app.use(cookieParser(process.env.COOKIE_SECRET));
29 | app.use(express.static(path.join(__dirname, 'public')));
30 |
31 | app.use('/api/auth', authRoutes);
32 | app.use('/api/users', usersRoutes);
33 |
34 | app.get('*', (req, res) => {
35 | res.sendFile(path.resolve(__dirname, 'public', 'index.html'));
36 | });
37 |
38 | app.use((req, res, next) => {
39 | const error = new Error('Not Found');
40 | error.status = 404;
41 | next(error);
42 | });
43 |
44 | app.use((error, req, res, next) => {
45 | console.error("\x1b[31m", error);
46 | if (res.headersSent) {
47 | return next(error);
48 | }
49 | return res.status(error.status || 500).json({
50 | error: {
51 | status: error.status || 500,
52 | message: error.status ? error.message : "Internal Server Error",
53 | },
54 | });
55 | });
56 |
57 | app.listen(PORT || 5000, (error) => {
58 | if (error) {
59 | console.log("Error in server setup");
60 | return;
61 | }
62 | console.log("Server listening on Port", PORT);
63 | });
--------------------------------------------------------------------------------
/client/src/components/Login/Login.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | background-color: rgb(0 128 128 / 10%);
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | padding: 40px 20px;
8 | }
9 |
10 | .formWrapper {
11 | max-width: 500px;
12 | width: 100%;
13 | }
14 |
15 | .form {
16 | background-color: #fff;
17 | box-shadow: 2px 2px 7px 2px rgb(0 0 0 / 20%);
18 | width: 100%;
19 | padding: 20px 30px;
20 | }
21 |
22 | .formTitle {
23 | color: teal;
24 | font-weight: 300;
25 | text-align: left;
26 | margin-bottom: 20px;
27 | font-size: 30px;
28 | }
29 |
30 | .formGroup {
31 | position: relative;
32 | margin: 10px 0;
33 | }
34 |
35 | .input {
36 | font-size: 16px;
37 | padding: 11px 12px;
38 | width: 100%;
39 | outline: 1px solid #d4d5d9;
40 | border: none;
41 | color: #282c3f;
42 | caret-color: teal;
43 | font-weight: 500;
44 |
45 | &:focus {
46 | outline: 1px solid teal;
47 | }
48 |
49 | @media (max-width: 768px) {
50 | font-size: 14px;
51 | }
52 |
53 | @media (max-width: 768px) {
54 | font-size: 14px;
55 | }
56 | }
57 |
58 | .submitButton {
59 | background: teal;
60 | color: white;
61 | border: 1px solid transparent;
62 | padding: 10px 20px;
63 | font-size: 14px;
64 | text-transform: uppercase;
65 | cursor: pointer;
66 |
67 | :disabled {
68 | background-color: grey;
69 | }
70 | }
71 |
72 | .validationError {
73 | color: red;
74 | height: 20px;
75 | }
76 |
77 | .text {
78 | color: #1b2839;
79 | font-weight: 500;
80 |
81 | @media (max-width: 768px) {
82 | font-size: 14px;
83 | }
84 |
85 | @media (max-width: 768px) {
86 | font-size: 14px;
87 | }
88 | }
89 |
90 | .link {
91 | color: teal;
92 | margin-left: 5px;
93 | }
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
28 |
29 | React App
30 |
31 |
32 | You need to enable JavaScript to run this app.
33 |
34 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/client/src/components/Signup/Signup.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | justify-content: center;
4 | background-color: rgb(0 128 128 / 10%);
5 | padding: 40px 20px;
6 | min-height: 100vh;
7 | }
8 |
9 | .formWrapper {
10 | display: flex;
11 | justify-content: center;
12 | box-sizing: border-box;
13 | align-items: center;
14 | width: 100%;
15 | }
16 |
17 | .form {
18 | padding: 20px 30px;
19 | width: 500px;
20 | background: #fff;
21 | box-shadow: 2px 2px 7px 2px rgb(0 0 0 / 20%);
22 | margin-right: 10px;
23 |
24 | @media (max-width: 768px) {
25 | width: 100%;
26 | }
27 | }
28 |
29 | .formTitle {
30 | color: teal;
31 | font-weight: 300;
32 | text-align: left;
33 | margin-bottom: 20px;
34 | font-size: 30px;
35 |
36 | @media (max-width: 768px) {
37 | font-size: 22px;
38 | }
39 | }
40 |
41 | .formGroup {
42 | position: relative;
43 | margin: 10px 0;
44 | }
45 |
46 | .input {
47 | font-size: 16px;
48 | padding: 11px 12px;
49 | width: 100%;
50 | outline: 1px solid #d4d5d9;
51 | border: none;
52 | color: #282c3f;
53 | caret-color: teal;
54 | font-weight: 500;
55 |
56 | &:focus {
57 | outline: 1px solid teal;
58 | }
59 |
60 | @media (max-width: 768px) {
61 | font-size: 14px;
62 | }
63 |
64 | @media (max-width: 768px) {
65 | font-size: 14px;
66 | }
67 | }
68 |
69 | .submitButton {
70 | background: teal;
71 | color: white;
72 | border: 1px solid transparent;
73 | padding: 10px 20px;
74 | font-size: 14px;
75 | text-transform: uppercase;
76 | cursor: pointer;
77 | }
78 |
79 | .validationError {
80 | color: red;
81 | height: 20px;
82 | }
83 |
84 | .text {
85 | color: #1b2839;
86 | font-weight: 500;
87 |
88 | @media (max-width: 768px) {
89 | font-size: 14px;
90 | }
91 |
92 | @media (max-width: 768px) {
93 | font-size: 14px;
94 | }
95 | }
96 |
97 | .link {
98 | color: teal;
99 | margin-left: 5px;
100 | }
--------------------------------------------------------------------------------
/client/src/components/Navbar/Navbar.module.scss:
--------------------------------------------------------------------------------
1 | .header {
2 | height: 70px;
3 | width: 100%;
4 | background-color: white;
5 | display: flex;
6 | align-items: center;
7 | padding: 0 20px;
8 | -webkit-box-shadow: 0px 8px 5px 0px rgba(0, 0, 0, 0.05);
9 | -moz-box-shadow: 0px 8px 5px 0px rgba(0, 0, 0, 0.05);
10 | box-shadow: 0px 8px 5px 0px rgba(0, 0, 0, 0.05);
11 | position: fixed;
12 | top: 0;
13 | left: 0;
14 | right: 0;
15 | z-index: 100;
16 |
17 | @media (max-width: 768px) {
18 | height: 60px;
19 | }
20 | }
21 |
22 | .navigation {
23 | display: flex;
24 | align-items: center;
25 | justify-content: space-between;
26 | flex: 1;
27 | }
28 |
29 | .brand {
30 | color: #1b2839;
31 | text-decoration: none;
32 | font-size: 24px;
33 | font-weight: 700;
34 | text-transform: uppercase;
35 |
36 | &:hover {
37 | color: teal;
38 | }
39 |
40 | @media (max-width: 768px) {
41 | font-size: 22px;
42 | }
43 | }
44 |
45 | .navigationList {
46 | display: flex;
47 | align-items: center;
48 | justify-content: flex-end;
49 | list-style: none;
50 | flex-basis: 33%;
51 |
52 | @media (max-width: 768px) {
53 | flex-direction: column;
54 | }
55 | }
56 |
57 | .navigationItem {
58 | margin-right: 20px;
59 | transition: all 5s;
60 |
61 | @media (max-width: 768px) {
62 | margin-right: 0;
63 | margin-bottom: 30px;
64 | }
65 | }
66 |
67 | .navigationLink {
68 | display: flex;
69 | flex-direction: column;
70 | align-items: center;
71 | justify-content: center;
72 | color: #1b2839;
73 | cursor: pointer;
74 | text-transform: uppercase;
75 | text-decoration: none;
76 | font-size: 14px;
77 |
78 | &:hover {
79 | color: teal;
80 | }
81 |
82 | &:hover svg {
83 | stroke: teal;
84 | }
85 |
86 | &:hover>span {
87 | color: teal;
88 | }
89 |
90 | // button styles
91 | background: none;
92 | border: none;
93 |
94 | @media (max-width: 768px) {
95 | flex-direction: row;
96 |
97 | & svg {
98 | margin-right: 5px;
99 | }
100 | }
101 | }
--------------------------------------------------------------------------------
/client/src/components/Navbar/Navbar.js:
--------------------------------------------------------------------------------
1 | import { Link, useNavigate } from "react-router-dom";
2 | import axios from "axios";
3 |
4 | import { useAuth } from "../../contexts/auth-context";
5 |
6 | import styles from "./Navbar.module.scss";
7 |
8 | const Navbar = () => {
9 | const { isAuthenticated, token, logout } = useAuth();
10 | const navigate = useNavigate();
11 |
12 | const logOutHandler = async () => {
13 | try {
14 | await axios.post(
15 | "/api/auth/logout",
16 | {},
17 | {
18 | headers: {
19 | Authorization: `Bearer ${token}`,
20 | },
21 | }
22 | );
23 | logout();
24 | navigate('/login');
25 | } catch (error) {
26 | console.log("Something went wrong.", error);
27 | }
28 | };
29 |
30 | return (
31 |
75 | );
76 | };
77 |
78 | export default Navbar;
--------------------------------------------------------------------------------
/client/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/contexts/auth-context.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { STATUS } from '../utils/utils';
5 |
6 | const initialState = {
7 | user: {},
8 | token: null,
9 | expiresAt: null,
10 | isAuthenticated: false,
11 | status: STATUS.PENDING,
12 | };
13 |
14 | const AuthContext = React.createContext({
15 | ...initialState,
16 | login: (user = {}, token = '', expiresAt = '') => {},
17 | logout: () => {},
18 | updateUser: () => {},
19 | setAuthenticationStatus: () => {},
20 | });
21 |
22 | const authReducer = (state, action) => {
23 | switch (action.type) {
24 | case 'login': {
25 | return {
26 | user: action.payload.user,
27 | token: action.payload.token,
28 | expiresAt: action.payload.expiresAt,
29 | isAuthenticated: true,
30 | verifyingToken: false,
31 | status: STATUS.SUCCEEDED,
32 | };
33 | }
34 | case 'logout': {
35 | return {
36 | ...initialState,
37 | status: STATUS.IDLE,
38 | };
39 | }
40 | case 'updateUser': {
41 | return {
42 | ...state,
43 | user: action.payload.user,
44 | };
45 | }
46 | case 'status': {
47 | return {
48 | ...state,
49 | status: action.payload.status,
50 | };
51 | }
52 | default: {
53 | throw new Error(`Unhandled action type: ${action.type}`);
54 | }
55 | }
56 | };
57 |
58 | const AuthProvider = ({ children }) => {
59 | const [state, dispatch] = React.useReducer(authReducer, initialState);
60 |
61 | const login = React.useCallback((user, token, expiresAt) => {
62 | dispatch({
63 | type: 'login',
64 | payload: {
65 | user,
66 | token,
67 | expiresAt,
68 | },
69 | });
70 | }, []);
71 |
72 | const logout = React.useCallback(() => {
73 | dispatch({
74 | type: 'logout',
75 | });
76 | }, []);
77 |
78 | const updateUser = React.useCallback((user) => {
79 | dispatch({
80 | type: 'updateUser',
81 | payload: {
82 | user,
83 | },
84 | });
85 | }, []);
86 |
87 | const setAuthenticationStatus = React.useCallback((status) => {
88 | dispatch({
89 | type: 'status',
90 | payload: {
91 | status,
92 | },
93 | });
94 | }, []);
95 |
96 | const value = React.useMemo(
97 | () => ({ ...state, login, logout, updateUser, setAuthenticationStatus }),
98 | [state, setAuthenticationStatus, login, logout, updateUser]
99 | );
100 |
101 | return {children} ;
102 | };
103 |
104 | const useAuth = () => {
105 | const context = React.useContext(AuthContext);
106 |
107 | if (context === undefined) {
108 | throw new Error('useAuth must be used within a AuthProvider');
109 | }
110 |
111 | return context;
112 | };
113 |
114 | AuthProvider.propTypes = {
115 | children: PropTypes.element.isRequired,
116 | };
117 |
118 | export { AuthProvider, useAuth };
--------------------------------------------------------------------------------
/server/middlewares/auth.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const createError = require('http-errors');
3 | const ms = require('ms');
4 |
5 | const { generateJWT } = require('../utils/auth');
6 |
7 | const { ACCESS_TOKEN_LIFE, REFRESH_TOKEN_LIFE, ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, NODE_ENV } = process.env;
8 |
9 | const dev = NODE_ENV === 'development';
10 |
11 | const { users, tokens } = require('../data/data');
12 |
13 | const generateAuthTokens = async (req, res, next) => {
14 | try {
15 | const user = users.find(user => user.id === req.userId);
16 |
17 | const refreshToken = generateJWT(
18 | req.userId,
19 | REFRESH_TOKEN_SECRET,
20 | REFRESH_TOKEN_LIFE
21 | );
22 |
23 | const accessToken = generateJWT(
24 | req.userId,
25 | ACCESS_TOKEN_SECRET,
26 | ACCESS_TOKEN_LIFE
27 | );
28 |
29 | const token = {
30 | refreshToken,
31 | userId: req.userId,
32 | expirationTime: new Date(Date.now() + ms(REFRESH_TOKEN_LIFE)).getTime(),
33 | };
34 |
35 | tokens.push(token);
36 |
37 | res.cookie("refreshToken", refreshToken, {
38 | httpOnly: true,
39 | secure: !dev,
40 | signed: true,
41 | expires: new Date(Date.now() + ms(REFRESH_TOKEN_LIFE)),
42 | });
43 |
44 | const expiresAt = new Date(Date.now() + ms(ACCESS_TOKEN_LIFE));
45 |
46 | return res.status(200).json({
47 | user,
48 | token: accessToken,
49 | expiresAt,
50 | });
51 | } catch (error) {
52 | return next(error);
53 | }
54 | };
55 |
56 |
57 | const isAuthenticated = async (req, res, next) => {
58 | try {
59 | const authToken = req.get('Authorization');
60 | const accessToken = authToken?.split('Bearer ')[1];
61 | if (!accessToken) {
62 | const error = createError.Unauthorized();
63 | throw error;
64 | }
65 |
66 | const { signedCookies = {} } = req;
67 |
68 | const { refreshToken } = signedCookies;
69 | if (!refreshToken) {
70 | const error = createError.Unauthorized();
71 | throw error;
72 | }
73 |
74 | let refreshTokenInDB = tokens.find(token => token.refreshToken === refreshToken);
75 |
76 | if (!refreshTokenInDB) {
77 | const error = createError.Unauthorized();
78 | throw error;
79 | }
80 |
81 | refreshTokenInDB = refreshTokenInDB.refreshToken;
82 |
83 | let decodedToken;
84 | try {
85 | decodedToken = jwt.verify(accessToken, ACCESS_TOKEN_SECRET);
86 | } catch (err) {
87 | const error = createError.Unauthorized();
88 | return next(error);
89 | }
90 |
91 | const { userId } = decodedToken;
92 |
93 | const user = users.find(user => user.id == userId);
94 | if (!user) {
95 | const error = createError.Unauthorized();
96 | throw error;
97 | }
98 |
99 | req.userId = user.id;
100 | return next();
101 | } catch (error) {
102 | return next(error);
103 | }
104 | };
105 |
106 | module.exports = {
107 | generateAuthTokens,
108 | isAuthenticated
109 | }
--------------------------------------------------------------------------------
/client/src/components/Login/Login.js:
--------------------------------------------------------------------------------
1 | import { Link, useNavigate } from "react-router-dom";
2 | import { useForm } from "react-hook-form";
3 | import axios from "axios";
4 |
5 | import { useAuth } from "../../contexts/auth-context";
6 | import { STATUS } from "../../utils/utils";
7 |
8 | import styles from "./Login.module.scss";
9 |
10 | const Login = () => {
11 | const {
12 | handleSubmit,
13 | register,
14 | formState: { errors, touchedFields },
15 | } = useForm({
16 | defaultValues: {
17 | username: "",
18 | password: "",
19 | },
20 | mode: "onChange",
21 | });
22 |
23 | const navigate = useNavigate();
24 |
25 | const { login, setAuthenticationStatus } = useAuth();
26 |
27 | const onSubmit = async (values) => {
28 | const user = {
29 | username: values.username,
30 | password: values.password,
31 | };
32 |
33 | try {
34 | setAuthenticationStatus(STATUS.PENDING);
35 | const response = await axios.post("/api/auth/login", user);
36 | setAuthenticationStatus(STATUS.SUCCEEDED);
37 | const { user: userObj, token, expiresAt } = response.data;
38 | login(userObj, token, expiresAt);
39 | navigate('/');
40 | } catch (error) {
41 | alert(error.response.data.error.message);
42 | }
43 | };
44 |
45 | return (
46 |
97 | );
98 | };
99 |
100 | export default Login;
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect } from "react";
2 | import {
3 | createBrowserRouter,
4 | RouterProvider,
5 | useLocation,
6 | Navigate,
7 | } from "react-router-dom";
8 | import PropTypes from "prop-types";
9 | import axios from "axios";
10 |
11 | import Signup from "./components/Signup/Signup";
12 | import Login from "./components/Login/Login";
13 | import Home from "./components/Home/Home";
14 | import Users from "./components/Users/Users";
15 | import Layout from "./components/Layout/Layout";
16 | import SplashScreen from "./components/SplashScreen/SplashScreen";
17 |
18 | import { useAuth } from "./contexts/auth-context";
19 | import { STATUS } from "./utils/utils";
20 |
21 | function App() {
22 | const { login, logout, isAuthenticated, expiresAt } = useAuth();
23 |
24 | const refreshAccessToken = useCallback(async () => {
25 | try {
26 | const response = await axios.post(
27 | "/api/auth/refresh",
28 | {},
29 | {
30 | withCredentials: true,
31 | }
32 | );
33 |
34 | const { user, accessToken, expiresAt } = response.data;
35 |
36 | if (response.status === 204) {
37 | logout();
38 | } else {
39 | login(user, accessToken, expiresAt);
40 | }
41 | } catch (error) {
42 | logout();
43 | }
44 | }, [login, logout]);
45 |
46 | useEffect(() => {
47 | refreshAccessToken();
48 | }, [refreshAccessToken]);
49 |
50 | useEffect(() => {
51 | let refreshAccessTokenTimerId;
52 |
53 | if (isAuthenticated) {
54 | refreshAccessTokenTimerId = setTimeout(() => {
55 | refreshAccessToken();
56 | }, new Date(expiresAt).getTime() - Date.now() - 10 * 1000);
57 | }
58 |
59 | return () => {
60 | if (isAuthenticated && refreshAccessTokenTimerId) {
61 | clearTimeout(refreshAccessTokenTimerId);
62 | }
63 | };
64 | }, [expiresAt, isAuthenticated, refreshAccessToken]);
65 |
66 | const router = createBrowserRouter([
67 | {
68 | element: ,
69 | children: [
70 | {
71 | path: "/",
72 | element: (
73 |
74 |
75 |
76 | ),
77 | },
78 | {
79 | path: "sign-up",
80 | element: (
81 |
82 |
83 |
84 | ),
85 | },
86 | {
87 | path: "login",
88 | element: (
89 |
90 |
91 |
92 | ),
93 | },
94 | {
95 | path: "users",
96 | element: (
97 |
98 |
99 |
100 | ),
101 | }
102 | ],
103 | },
104 | ]);
105 |
106 | return (
107 |
108 |
109 |
110 | );
111 | }
112 |
113 | const RequireAuth = ({ children, redirectTo }) => {
114 | const { isAuthenticated, status } = useAuth();
115 | const location = useLocation();
116 |
117 | if (status === STATUS.PENDING) return ;
118 |
119 | return isAuthenticated ? (
120 | children
121 | ) : (
122 |
123 | );
124 | };
125 |
126 | const RedirectIfLoggedIn = ({ children, redirectTo }) => {
127 | const { isAuthenticated, status } = useAuth();
128 | const location = useLocation();
129 |
130 | if (status === STATUS.PENDING) return ;
131 |
132 | return isAuthenticated ? (
133 |
134 | ) : (
135 | children
136 | );
137 | };
138 |
139 | RequireAuth.propTypes = {
140 | children: PropTypes.element.isRequired,
141 | redirectTo: PropTypes.string.isRequired,
142 | };
143 |
144 | RedirectIfLoggedIn.propTypes = {
145 | children: PropTypes.element.isRequired,
146 | redirectTo: PropTypes.string.isRequired,
147 | };
148 |
149 | export default App;
--------------------------------------------------------------------------------
/server/controllers/auth.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const createError = require("http-errors");
3 | const ms = require('ms');
4 |
5 | const { clearTokens, generateJWT } = require("../utils/auth");
6 | const { users, tokens } = require("../data/data");
7 |
8 | const signUp = async (req, res, next) => {
9 | const { name, username, email, password } = req.body;
10 |
11 | if(!name || !username || !email || !password) {
12 | return res.status(422).json({
13 | error: 'Please fill all the required fields'
14 | });
15 | }
16 |
17 | try {
18 | const userAlreadyExists = users.find(user => {
19 | if (user.userName === username || user.email === email) {
20 | return true;
21 | }
22 | return false;
23 | });
24 |
25 | if (userAlreadyExists) {
26 | return res.status(422).json({
27 | error: 'Username or email already exists'
28 | });
29 | }
30 |
31 | const newUser = {
32 | id: users[users.length - 1].id + 1,
33 | name: name,
34 | userName: username,
35 | email: email,
36 | password: password
37 | };
38 |
39 | users.push(newUser);
40 |
41 | req.userId = newUser.id;
42 | return next();
43 | } catch (error) {
44 | return next(error);
45 | }
46 | };
47 |
48 | const login = async (req, res, next) => {
49 | const { username, password } = req.body;
50 |
51 | try {
52 | if (!username || !password) {
53 | return res.status(422).json({
54 | error: 'Please fill all the required fields'
55 | });
56 | }
57 |
58 | const user = users.find(user => {
59 | if (user.userName === username || user.email === username) {
60 | return true;
61 | }
62 | return false;
63 | });
64 |
65 | if (!user) {
66 | const error = createError.Unauthorized('Invalid username or password');
67 | throw error;
68 | }
69 |
70 | const passwordsMatch = user.password == password;
71 |
72 | if (!passwordsMatch) {
73 | const error = createError.Unauthorized('Invalid username or password');
74 | throw error;
75 | }
76 |
77 | req.userId = user.id;
78 | return next();
79 | } catch (error) {
80 | return next(error);
81 | }
82 | };
83 |
84 | const logout = async (req, res, next) => {
85 | await clearTokens(req, res, next);
86 | return res.sendStatus(204);
87 | };
88 |
89 | const refreshAccessToken = async (req, res, next) => {
90 | const { REFRESH_TOKEN_SECRET, ACCESS_TOKEN_SECRET, ACCESS_TOKEN_LIFE } = process.env;
91 |
92 | const { signedCookies } = req;
93 | const { refreshToken } = signedCookies;
94 | if (!refreshToken) {
95 | return res.sendStatus(204);
96 | }
97 | try {
98 | const refreshTokenInDB = tokens.find(token => token.refreshToken == refreshToken)?.refreshToken;
99 |
100 | if (!refreshTokenInDB) {
101 | await clearTokens(req, res, next);
102 | const error = createError.Unauthorized();
103 | throw error;
104 | }
105 |
106 | try {
107 | const decodedToken = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
108 | const { userId } = decodedToken;
109 | const user = users.find(user => user.id == userId);
110 |
111 | if (!user) {
112 | await clearTokens(req, res);
113 | const error = createError("Invalid credentials", 401);
114 | throw error;
115 | }
116 |
117 | const accessToken = generateJWT(
118 | user.id,
119 | ACCESS_TOKEN_SECRET,
120 | ACCESS_TOKEN_LIFE
121 | );
122 | return res.status(200).json({
123 | user,
124 | accessToken,
125 | expiresAt: new Date(Date.now() + ms(ACCESS_TOKEN_LIFE)),
126 | });
127 | } catch (error) {
128 | return next(error);
129 | }
130 | } catch (error) {
131 | return next(error);
132 | }
133 | };
134 |
135 | module.exports = {
136 | signUp,
137 | login,
138 | logout,
139 | refreshAccessToken
140 | };
--------------------------------------------------------------------------------
/client/src/components/Signup/Signup.js:
--------------------------------------------------------------------------------
1 | import { Link, useNavigate } from "react-router-dom";
2 | import { useForm } from "react-hook-form";
3 | import axios from "axios";
4 |
5 | import { useAuth } from "../../contexts/auth-context";
6 | import { STATUS } from "../../utils/utils";
7 |
8 | import styles from "./Signup.module.scss";
9 |
10 | const Signup = () => {
11 |
12 | const {
13 | handleSubmit,
14 | register,
15 | formState: { errors, touchedFields },
16 | } = useForm({
17 | defaultValues: {
18 | name: "",
19 | username: "",
20 | email: "",
21 | password: "",
22 | confirmPassword: "",
23 | },
24 | mode: "onChange",
25 | });
26 |
27 | const navigate = useNavigate();
28 |
29 | const { login, setAuthenticationStatus } = useAuth();
30 |
31 | const onSubmit = async (values) => {
32 | const newUser = {
33 | name: values.name,
34 | username: values.username,
35 | email: values.email,
36 | password: values.password,
37 | confirmPassword: values.confirmPassword,
38 | };
39 |
40 | try {
41 | setAuthenticationStatus(STATUS.PENDING);
42 | const response = await axios.post("/api/auth/sign-up", newUser);
43 | setAuthenticationStatus(STATUS.SUCCEEDED);
44 | const { user, token, expiresAt } = response.data;
45 | login(user, token, expiresAt);
46 | navigate("/");
47 | } catch (error) {
48 | alert(error.response.data.error.message);
49 | setAuthenticationStatus(STATUS.FAILED);
50 | }
51 | };
52 |
53 | return (
54 |
186 | );
187 | };
188 |
189 | export default Signup;
--------------------------------------------------------------------------------
/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "server",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "cookie-parser": "^1.4.6",
13 | "cors": "^2.8.5",
14 | "dotenv": "^16.0.3",
15 | "express": "^4.18.2",
16 | "http-errors": "^2.0.0",
17 | "jsonwebtoken": "^9.0.0",
18 | "ms": "^2.1.3"
19 | }
20 | },
21 | "node_modules/accepts": {
22 | "version": "1.3.8",
23 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
24 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
25 | "dependencies": {
26 | "mime-types": "~2.1.34",
27 | "negotiator": "0.6.3"
28 | },
29 | "engines": {
30 | "node": ">= 0.6"
31 | }
32 | },
33 | "node_modules/array-flatten": {
34 | "version": "1.1.1",
35 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
36 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
37 | },
38 | "node_modules/body-parser": {
39 | "version": "1.20.1",
40 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
41 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
42 | "dependencies": {
43 | "bytes": "3.1.2",
44 | "content-type": "~1.0.4",
45 | "debug": "2.6.9",
46 | "depd": "2.0.0",
47 | "destroy": "1.2.0",
48 | "http-errors": "2.0.0",
49 | "iconv-lite": "0.4.24",
50 | "on-finished": "2.4.1",
51 | "qs": "6.11.0",
52 | "raw-body": "2.5.1",
53 | "type-is": "~1.6.18",
54 | "unpipe": "1.0.0"
55 | },
56 | "engines": {
57 | "node": ">= 0.8",
58 | "npm": "1.2.8000 || >= 1.4.16"
59 | }
60 | },
61 | "node_modules/buffer-equal-constant-time": {
62 | "version": "1.0.1",
63 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
64 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
65 | },
66 | "node_modules/bytes": {
67 | "version": "3.1.2",
68 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
69 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
70 | "engines": {
71 | "node": ">= 0.8"
72 | }
73 | },
74 | "node_modules/call-bind": {
75 | "version": "1.0.2",
76 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
77 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
78 | "dependencies": {
79 | "function-bind": "^1.1.1",
80 | "get-intrinsic": "^1.0.2"
81 | },
82 | "funding": {
83 | "url": "https://github.com/sponsors/ljharb"
84 | }
85 | },
86 | "node_modules/content-disposition": {
87 | "version": "0.5.4",
88 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
89 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
90 | "dependencies": {
91 | "safe-buffer": "5.2.1"
92 | },
93 | "engines": {
94 | "node": ">= 0.6"
95 | }
96 | },
97 | "node_modules/content-type": {
98 | "version": "1.0.5",
99 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
100 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
101 | "engines": {
102 | "node": ">= 0.6"
103 | }
104 | },
105 | "node_modules/cookie": {
106 | "version": "0.4.1",
107 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
108 | "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
109 | "engines": {
110 | "node": ">= 0.6"
111 | }
112 | },
113 | "node_modules/cookie-parser": {
114 | "version": "1.4.6",
115 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
116 | "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
117 | "dependencies": {
118 | "cookie": "0.4.1",
119 | "cookie-signature": "1.0.6"
120 | },
121 | "engines": {
122 | "node": ">= 0.8.0"
123 | }
124 | },
125 | "node_modules/cookie-signature": {
126 | "version": "1.0.6",
127 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
128 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
129 | },
130 | "node_modules/cors": {
131 | "version": "2.8.5",
132 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
133 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
134 | "dependencies": {
135 | "object-assign": "^4",
136 | "vary": "^1"
137 | },
138 | "engines": {
139 | "node": ">= 0.10"
140 | }
141 | },
142 | "node_modules/debug": {
143 | "version": "2.6.9",
144 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
145 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
146 | "dependencies": {
147 | "ms": "2.0.0"
148 | }
149 | },
150 | "node_modules/debug/node_modules/ms": {
151 | "version": "2.0.0",
152 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
153 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
154 | },
155 | "node_modules/depd": {
156 | "version": "2.0.0",
157 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
158 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
159 | "engines": {
160 | "node": ">= 0.8"
161 | }
162 | },
163 | "node_modules/destroy": {
164 | "version": "1.2.0",
165 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
166 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
167 | "engines": {
168 | "node": ">= 0.8",
169 | "npm": "1.2.8000 || >= 1.4.16"
170 | }
171 | },
172 | "node_modules/dotenv": {
173 | "version": "16.0.3",
174 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
175 | "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
176 | "engines": {
177 | "node": ">=12"
178 | }
179 | },
180 | "node_modules/ecdsa-sig-formatter": {
181 | "version": "1.0.11",
182 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
183 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
184 | "dependencies": {
185 | "safe-buffer": "^5.0.1"
186 | }
187 | },
188 | "node_modules/ee-first": {
189 | "version": "1.1.1",
190 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
191 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
192 | },
193 | "node_modules/encodeurl": {
194 | "version": "1.0.2",
195 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
196 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
197 | "engines": {
198 | "node": ">= 0.8"
199 | }
200 | },
201 | "node_modules/escape-html": {
202 | "version": "1.0.3",
203 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
204 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
205 | },
206 | "node_modules/etag": {
207 | "version": "1.8.1",
208 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
209 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
210 | "engines": {
211 | "node": ">= 0.6"
212 | }
213 | },
214 | "node_modules/express": {
215 | "version": "4.18.2",
216 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
217 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
218 | "dependencies": {
219 | "accepts": "~1.3.8",
220 | "array-flatten": "1.1.1",
221 | "body-parser": "1.20.1",
222 | "content-disposition": "0.5.4",
223 | "content-type": "~1.0.4",
224 | "cookie": "0.5.0",
225 | "cookie-signature": "1.0.6",
226 | "debug": "2.6.9",
227 | "depd": "2.0.0",
228 | "encodeurl": "~1.0.2",
229 | "escape-html": "~1.0.3",
230 | "etag": "~1.8.1",
231 | "finalhandler": "1.2.0",
232 | "fresh": "0.5.2",
233 | "http-errors": "2.0.0",
234 | "merge-descriptors": "1.0.1",
235 | "methods": "~1.1.2",
236 | "on-finished": "2.4.1",
237 | "parseurl": "~1.3.3",
238 | "path-to-regexp": "0.1.7",
239 | "proxy-addr": "~2.0.7",
240 | "qs": "6.11.0",
241 | "range-parser": "~1.2.1",
242 | "safe-buffer": "5.2.1",
243 | "send": "0.18.0",
244 | "serve-static": "1.15.0",
245 | "setprototypeof": "1.2.0",
246 | "statuses": "2.0.1",
247 | "type-is": "~1.6.18",
248 | "utils-merge": "1.0.1",
249 | "vary": "~1.1.2"
250 | },
251 | "engines": {
252 | "node": ">= 0.10.0"
253 | }
254 | },
255 | "node_modules/express/node_modules/cookie": {
256 | "version": "0.5.0",
257 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
258 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
259 | "engines": {
260 | "node": ">= 0.6"
261 | }
262 | },
263 | "node_modules/finalhandler": {
264 | "version": "1.2.0",
265 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
266 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
267 | "dependencies": {
268 | "debug": "2.6.9",
269 | "encodeurl": "~1.0.2",
270 | "escape-html": "~1.0.3",
271 | "on-finished": "2.4.1",
272 | "parseurl": "~1.3.3",
273 | "statuses": "2.0.1",
274 | "unpipe": "~1.0.0"
275 | },
276 | "engines": {
277 | "node": ">= 0.8"
278 | }
279 | },
280 | "node_modules/forwarded": {
281 | "version": "0.2.0",
282 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
283 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
284 | "engines": {
285 | "node": ">= 0.6"
286 | }
287 | },
288 | "node_modules/fresh": {
289 | "version": "0.5.2",
290 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
291 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
292 | "engines": {
293 | "node": ">= 0.6"
294 | }
295 | },
296 | "node_modules/function-bind": {
297 | "version": "1.1.1",
298 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
299 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
300 | },
301 | "node_modules/get-intrinsic": {
302 | "version": "1.2.1",
303 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
304 | "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
305 | "dependencies": {
306 | "function-bind": "^1.1.1",
307 | "has": "^1.0.3",
308 | "has-proto": "^1.0.1",
309 | "has-symbols": "^1.0.3"
310 | },
311 | "funding": {
312 | "url": "https://github.com/sponsors/ljharb"
313 | }
314 | },
315 | "node_modules/has": {
316 | "version": "1.0.3",
317 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
318 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
319 | "dependencies": {
320 | "function-bind": "^1.1.1"
321 | },
322 | "engines": {
323 | "node": ">= 0.4.0"
324 | }
325 | },
326 | "node_modules/has-proto": {
327 | "version": "1.0.1",
328 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
329 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
330 | "engines": {
331 | "node": ">= 0.4"
332 | },
333 | "funding": {
334 | "url": "https://github.com/sponsors/ljharb"
335 | }
336 | },
337 | "node_modules/has-symbols": {
338 | "version": "1.0.3",
339 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
340 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
341 | "engines": {
342 | "node": ">= 0.4"
343 | },
344 | "funding": {
345 | "url": "https://github.com/sponsors/ljharb"
346 | }
347 | },
348 | "node_modules/http-errors": {
349 | "version": "2.0.0",
350 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
351 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
352 | "dependencies": {
353 | "depd": "2.0.0",
354 | "inherits": "2.0.4",
355 | "setprototypeof": "1.2.0",
356 | "statuses": "2.0.1",
357 | "toidentifier": "1.0.1"
358 | },
359 | "engines": {
360 | "node": ">= 0.8"
361 | }
362 | },
363 | "node_modules/iconv-lite": {
364 | "version": "0.4.24",
365 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
366 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
367 | "dependencies": {
368 | "safer-buffer": ">= 2.1.2 < 3"
369 | },
370 | "engines": {
371 | "node": ">=0.10.0"
372 | }
373 | },
374 | "node_modules/inherits": {
375 | "version": "2.0.4",
376 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
377 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
378 | },
379 | "node_modules/ipaddr.js": {
380 | "version": "1.9.1",
381 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
382 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
383 | "engines": {
384 | "node": ">= 0.10"
385 | }
386 | },
387 | "node_modules/jsonwebtoken": {
388 | "version": "9.0.0",
389 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz",
390 | "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==",
391 | "dependencies": {
392 | "jws": "^3.2.2",
393 | "lodash": "^4.17.21",
394 | "ms": "^2.1.1",
395 | "semver": "^7.3.8"
396 | },
397 | "engines": {
398 | "node": ">=12",
399 | "npm": ">=6"
400 | }
401 | },
402 | "node_modules/jwa": {
403 | "version": "1.4.1",
404 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
405 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
406 | "dependencies": {
407 | "buffer-equal-constant-time": "1.0.1",
408 | "ecdsa-sig-formatter": "1.0.11",
409 | "safe-buffer": "^5.0.1"
410 | }
411 | },
412 | "node_modules/jws": {
413 | "version": "3.2.2",
414 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
415 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
416 | "dependencies": {
417 | "jwa": "^1.4.1",
418 | "safe-buffer": "^5.0.1"
419 | }
420 | },
421 | "node_modules/lodash": {
422 | "version": "4.17.21",
423 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
424 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
425 | },
426 | "node_modules/lru-cache": {
427 | "version": "6.0.0",
428 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
429 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
430 | "dependencies": {
431 | "yallist": "^4.0.0"
432 | },
433 | "engines": {
434 | "node": ">=10"
435 | }
436 | },
437 | "node_modules/media-typer": {
438 | "version": "0.3.0",
439 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
440 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
441 | "engines": {
442 | "node": ">= 0.6"
443 | }
444 | },
445 | "node_modules/merge-descriptors": {
446 | "version": "1.0.1",
447 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
448 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
449 | },
450 | "node_modules/methods": {
451 | "version": "1.1.2",
452 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
453 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
454 | "engines": {
455 | "node": ">= 0.6"
456 | }
457 | },
458 | "node_modules/mime": {
459 | "version": "1.6.0",
460 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
461 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
462 | "bin": {
463 | "mime": "cli.js"
464 | },
465 | "engines": {
466 | "node": ">=4"
467 | }
468 | },
469 | "node_modules/mime-db": {
470 | "version": "1.52.0",
471 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
472 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
473 | "engines": {
474 | "node": ">= 0.6"
475 | }
476 | },
477 | "node_modules/mime-types": {
478 | "version": "2.1.35",
479 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
480 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
481 | "dependencies": {
482 | "mime-db": "1.52.0"
483 | },
484 | "engines": {
485 | "node": ">= 0.6"
486 | }
487 | },
488 | "node_modules/ms": {
489 | "version": "2.1.3",
490 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
491 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
492 | },
493 | "node_modules/negotiator": {
494 | "version": "0.6.3",
495 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
496 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
497 | "engines": {
498 | "node": ">= 0.6"
499 | }
500 | },
501 | "node_modules/object-assign": {
502 | "version": "4.1.1",
503 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
504 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
505 | "engines": {
506 | "node": ">=0.10.0"
507 | }
508 | },
509 | "node_modules/object-inspect": {
510 | "version": "1.12.3",
511 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
512 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
513 | "funding": {
514 | "url": "https://github.com/sponsors/ljharb"
515 | }
516 | },
517 | "node_modules/on-finished": {
518 | "version": "2.4.1",
519 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
520 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
521 | "dependencies": {
522 | "ee-first": "1.1.1"
523 | },
524 | "engines": {
525 | "node": ">= 0.8"
526 | }
527 | },
528 | "node_modules/parseurl": {
529 | "version": "1.3.3",
530 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
531 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
532 | "engines": {
533 | "node": ">= 0.8"
534 | }
535 | },
536 | "node_modules/path-to-regexp": {
537 | "version": "0.1.7",
538 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
539 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
540 | },
541 | "node_modules/proxy-addr": {
542 | "version": "2.0.7",
543 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
544 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
545 | "dependencies": {
546 | "forwarded": "0.2.0",
547 | "ipaddr.js": "1.9.1"
548 | },
549 | "engines": {
550 | "node": ">= 0.10"
551 | }
552 | },
553 | "node_modules/qs": {
554 | "version": "6.11.0",
555 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
556 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
557 | "dependencies": {
558 | "side-channel": "^1.0.4"
559 | },
560 | "engines": {
561 | "node": ">=0.6"
562 | },
563 | "funding": {
564 | "url": "https://github.com/sponsors/ljharb"
565 | }
566 | },
567 | "node_modules/range-parser": {
568 | "version": "1.2.1",
569 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
570 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
571 | "engines": {
572 | "node": ">= 0.6"
573 | }
574 | },
575 | "node_modules/raw-body": {
576 | "version": "2.5.1",
577 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
578 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
579 | "dependencies": {
580 | "bytes": "3.1.2",
581 | "http-errors": "2.0.0",
582 | "iconv-lite": "0.4.24",
583 | "unpipe": "1.0.0"
584 | },
585 | "engines": {
586 | "node": ">= 0.8"
587 | }
588 | },
589 | "node_modules/safe-buffer": {
590 | "version": "5.2.1",
591 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
592 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
593 | "funding": [
594 | {
595 | "type": "github",
596 | "url": "https://github.com/sponsors/feross"
597 | },
598 | {
599 | "type": "patreon",
600 | "url": "https://www.patreon.com/feross"
601 | },
602 | {
603 | "type": "consulting",
604 | "url": "https://feross.org/support"
605 | }
606 | ]
607 | },
608 | "node_modules/safer-buffer": {
609 | "version": "2.1.2",
610 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
611 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
612 | },
613 | "node_modules/semver": {
614 | "version": "7.5.1",
615 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
616 | "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
617 | "dependencies": {
618 | "lru-cache": "^6.0.0"
619 | },
620 | "bin": {
621 | "semver": "bin/semver.js"
622 | },
623 | "engines": {
624 | "node": ">=10"
625 | }
626 | },
627 | "node_modules/send": {
628 | "version": "0.18.0",
629 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
630 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
631 | "dependencies": {
632 | "debug": "2.6.9",
633 | "depd": "2.0.0",
634 | "destroy": "1.2.0",
635 | "encodeurl": "~1.0.2",
636 | "escape-html": "~1.0.3",
637 | "etag": "~1.8.1",
638 | "fresh": "0.5.2",
639 | "http-errors": "2.0.0",
640 | "mime": "1.6.0",
641 | "ms": "2.1.3",
642 | "on-finished": "2.4.1",
643 | "range-parser": "~1.2.1",
644 | "statuses": "2.0.1"
645 | },
646 | "engines": {
647 | "node": ">= 0.8.0"
648 | }
649 | },
650 | "node_modules/serve-static": {
651 | "version": "1.15.0",
652 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
653 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
654 | "dependencies": {
655 | "encodeurl": "~1.0.2",
656 | "escape-html": "~1.0.3",
657 | "parseurl": "~1.3.3",
658 | "send": "0.18.0"
659 | },
660 | "engines": {
661 | "node": ">= 0.8.0"
662 | }
663 | },
664 | "node_modules/setprototypeof": {
665 | "version": "1.2.0",
666 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
667 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
668 | },
669 | "node_modules/side-channel": {
670 | "version": "1.0.4",
671 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
672 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
673 | "dependencies": {
674 | "call-bind": "^1.0.0",
675 | "get-intrinsic": "^1.0.2",
676 | "object-inspect": "^1.9.0"
677 | },
678 | "funding": {
679 | "url": "https://github.com/sponsors/ljharb"
680 | }
681 | },
682 | "node_modules/statuses": {
683 | "version": "2.0.1",
684 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
685 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
686 | "engines": {
687 | "node": ">= 0.8"
688 | }
689 | },
690 | "node_modules/toidentifier": {
691 | "version": "1.0.1",
692 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
693 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
694 | "engines": {
695 | "node": ">=0.6"
696 | }
697 | },
698 | "node_modules/type-is": {
699 | "version": "1.6.18",
700 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
701 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
702 | "dependencies": {
703 | "media-typer": "0.3.0",
704 | "mime-types": "~2.1.24"
705 | },
706 | "engines": {
707 | "node": ">= 0.6"
708 | }
709 | },
710 | "node_modules/unpipe": {
711 | "version": "1.0.0",
712 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
713 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
714 | "engines": {
715 | "node": ">= 0.8"
716 | }
717 | },
718 | "node_modules/utils-merge": {
719 | "version": "1.0.1",
720 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
721 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
722 | "engines": {
723 | "node": ">= 0.4.0"
724 | }
725 | },
726 | "node_modules/vary": {
727 | "version": "1.1.2",
728 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
729 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
730 | "engines": {
731 | "node": ">= 0.8"
732 | }
733 | },
734 | "node_modules/yallist": {
735 | "version": "4.0.0",
736 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
737 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
738 | }
739 | }
740 | }
741 |
--------------------------------------------------------------------------------