├── frontend
├── src
│ ├── components
│ │ ├── AllRoutes
│ │ │ ├── Loading
│ │ │ │ ├── Loading.module.css
│ │ │ │ └── Loading.tsx
│ │ │ ├── Layout
│ │ │ │ ├── Layout.module.css
│ │ │ │ └── Layout.tsx
│ │ │ ├── CustomInput
│ │ │ │ └── CustomInput.tsx
│ │ │ ├── CustomAlert
│ │ │ │ └── CustomAlert.tsx
│ │ │ ├── CustomLink
│ │ │ │ └── CustomLink.tsx
│ │ │ └── CustomButton
│ │ │ │ └── CustomButton.tsx
│ │ ├── Private
│ │ │ ├── Main
│ │ │ │ ├── Main.module.css
│ │ │ │ └── Main.tsx
│ │ │ ├── FlexContainer
│ │ │ │ ├── FlexContainer.module.css
│ │ │ │ └── FlexContainer.tsx
│ │ │ ├── Footer
│ │ │ │ ├── Footer.module.css
│ │ │ │ └── Footer.tsx
│ │ │ ├── Header
│ │ │ │ ├── Header.module.css
│ │ │ │ └── Header.tsx
│ │ │ └── HeaderDropdown
│ │ │ │ └── HeaderDropdown.tsx
│ │ └── Public
│ │ │ ├── GoogleIdentityServices
│ │ │ ├── Globals.d.ts
│ │ │ ├── GoogleIdentityServices.module.css
│ │ │ └── GoogleIdentityServices.tsx
│ │ │ ├── FirebaseGoogleSignInButton
│ │ │ ├── FirebaseGoogleSignInButton.module.css
│ │ │ └── FirebaseGoogleSignInButton.tsx
│ │ │ └── FirebaseFacebookSignInButton
│ │ │ ├── FirebaseFacebookSignInButton.module.css
│ │ │ └── FirebaseFacebookSignInButton.tsx
│ ├── Globals.d.ts
│ ├── assets
│ │ ├── github.png
│ │ ├── google.png
│ │ ├── facebook.png
│ │ ├── instagram.png
│ │ ├── linkedin.png
│ │ ├── logo-header.png
│ │ ├── landing_page_icon_1.png
│ │ ├── spinner-circle-dark.svg
│ │ ├── spinner-circle-light.svg
│ │ ├── landing_page_icon_3.svg
│ │ ├── landing_page_icon_4.svg
│ │ └── login-background.svg
│ ├── interfaces
│ │ └── index.ts
│ ├── pages
│ │ ├── Private
│ │ │ └── Home
│ │ │ │ ├── Home.module.css
│ │ │ │ └── Home.tsx
│ │ ├── Public
│ │ │ ├── AccountActivation
│ │ │ │ ├── AccountActivation.module.css
│ │ │ │ └── AccountActivation.tsx
│ │ │ ├── ForgotPassword
│ │ │ │ ├── ForgotPassword.module.css
│ │ │ │ └── ForgotPassword.tsx
│ │ │ ├── Login
│ │ │ │ ├── Login.module.css
│ │ │ │ └── Login.tsx
│ │ │ ├── ResetPassword
│ │ │ │ ├── ResetPassword.module.css
│ │ │ │ └── ResetPassword.tsx
│ │ │ └── Register
│ │ │ │ └── Register.module.css
│ │ └── MFA
│ │ │ └── LoginVerificationCode
│ │ │ ├── LoginVerificationCode.module.css
│ │ │ └── LoginVerificationCode.tsx
│ ├── reducers
│ │ ├── index.ts
│ │ ├── isDisableReducer.ts
│ │ └── errorReducer.ts
│ ├── actions
│ │ └── index.ts
│ ├── App.css
│ ├── config
│ │ └── firebase-config.ts
│ ├── routes
│ │ ├── MFARoutes.tsx
│ │ ├── PrivateRoutes.tsx
│ │ └── PublicRoutes.tsx
│ ├── helpers
│ │ └── auth.ts
│ ├── index.tsx
│ ├── App.tsx
│ └── App.module.css
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── index.html
├── tailwind.config.js
├── tsconfig.json
├── .env.example
└── package.json
├── backend
├── nodemon.json
├── src
│ ├── constants
│ │ ├── v1AuthenticationUserSettings.ts
│ │ ├── v1AuthenticationJWTTokensSettings.ts
│ │ ├── v1AuthenticationCookiesSettings.ts
│ │ └── v1AuthenticationErrorCodes.ts
│ ├── config
│ │ └── firebase-credential.example.json
│ ├── utils
│ │ ├── tryCatch.ts
│ │ ├── ErrorResponse.ts
│ │ ├── generateRandomUsernameSSO.ts
│ │ ├── generateRandomPasswordSSO.ts
│ │ └── sendEmail.ts
│ ├── models
│ │ ├── csrfTokenSecretModel.ts
│ │ ├── googleAuthenticatorModel.ts
│ │ ├── profileModel.ts
│ │ └── userModel.ts
│ ├── types
│ │ └── index.ts
│ ├── interfaces
│ │ └── index.ts
│ ├── middlewares
│ │ ├── errorHandler.ts
│ │ ├── v1AuthenticationLimiter.ts
│ │ └── index.ts
│ ├── routes
│ │ └── v1AuthenticationRouter.ts
│ └── server.ts
├── .env.example
├── Globals.d.ts
├── package.json
└── tsconfig.json
├── screenshots
├── image.png
├── image-1.png
├── image-2.png
├── image-3.png
├── image-4.png
├── image-5.png
├── image-6.png
├── image-7.png
├── image-8.png
└── image-9.png
├── .gitignore
└── README.md
/frontend/src/components/AllRoutes/Loading/Loading.module.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/Private/Main/Main.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | height: auto;
3 | }
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/backend/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["src"],
3 | "ext": ".ts,.js",
4 | "exec": "ts-node --esm ./src/server.ts"
5 | }
--------------------------------------------------------------------------------
/screenshots/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/screenshots/image.png
--------------------------------------------------------------------------------
/frontend/src/components/Public/GoogleIdentityServices/Globals.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 | declare global {
3 | const google: any;
4 | }
--------------------------------------------------------------------------------
/screenshots/image-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/screenshots/image-1.png
--------------------------------------------------------------------------------
/screenshots/image-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/screenshots/image-2.png
--------------------------------------------------------------------------------
/screenshots/image-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/screenshots/image-3.png
--------------------------------------------------------------------------------
/screenshots/image-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/screenshots/image-4.png
--------------------------------------------------------------------------------
/screenshots/image-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/screenshots/image-5.png
--------------------------------------------------------------------------------
/screenshots/image-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/screenshots/image-6.png
--------------------------------------------------------------------------------
/screenshots/image-7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/screenshots/image-7.png
--------------------------------------------------------------------------------
/screenshots/image-8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/screenshots/image-8.png
--------------------------------------------------------------------------------
/screenshots/image-9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/screenshots/image-9.png
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/src/Globals.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.module.css";
2 | declare module "*.module.scss";
3 | declare module "*.png";
4 | declare module "*.svg";
--------------------------------------------------------------------------------
/frontend/src/assets/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/frontend/src/assets/github.png
--------------------------------------------------------------------------------
/frontend/src/assets/google.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/frontend/src/assets/google.png
--------------------------------------------------------------------------------
/frontend/src/assets/facebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/frontend/src/assets/facebook.png
--------------------------------------------------------------------------------
/frontend/src/assets/instagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/frontend/src/assets/instagram.png
--------------------------------------------------------------------------------
/frontend/src/assets/linkedin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/frontend/src/assets/linkedin.png
--------------------------------------------------------------------------------
/frontend/src/assets/logo-header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/frontend/src/assets/logo-header.png
--------------------------------------------------------------------------------
/frontend/src/assets/landing_page_icon_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate/HEAD/frontend/src/assets/landing_page_icon_1.png
--------------------------------------------------------------------------------
/frontend/src/components/AllRoutes/Loading/Loading.tsx:
--------------------------------------------------------------------------------
1 | const Loading = () => {
2 | return (
3 | <>
4 | >
5 | )
6 | }
7 |
8 | export default Loading;
--------------------------------------------------------------------------------
/frontend/src/components/Private/FlexContainer/FlexContainer.module.css:
--------------------------------------------------------------------------------
1 | .flex_container {
2 | width: min(100% - 2rem, 1216px);
3 | margin: auto;
4 | height: 100%;
5 | }
--------------------------------------------------------------------------------
/frontend/src/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export interface AllReducers {
2 | error: {
3 | hasError: boolean;
4 | errorMessage: string;
5 | };
6 | isDisabled: boolean;
7 | }
--------------------------------------------------------------------------------
/frontend/src/components/Private/Footer/Footer.module.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | height: 3rem;
3 | width: 100%;
4 | height: 4rem;
5 | background-color: #1f2937;
6 | border-top: 1px solid #323c4b;
7 | }
--------------------------------------------------------------------------------
/frontend/src/pages/Private/Home/Home.module.css:
--------------------------------------------------------------------------------
1 | .box_message {
2 | padding: 1.5rem;
3 | background-color: #1f2937;
4 | width: 100%;
5 | margin-top: 3rem;
6 | margin-bottom: 3rem;
7 | border-radius: 0.5rem;
8 | }
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./src/**/*.{js,jsx,ts,tsx}",
5 | ],
6 | theme: {
7 | extend: {},
8 | },
9 | plugins: [],
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/backend/src/constants/v1AuthenticationUserSettings.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_PROFILE_PICTURE: string = "https://res.cloudinary.com/dgo6vnzjl/image/upload/c_fill,q_50,w_150/v1685085963/default_male_avatar_xkpekq.webp"; // * DEFAULT PROFILE PICTURE OF USER
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | /frontend/.env
4 | /frontend/node_modules
5 |
6 | /backend/node_modules
7 | /backend/dist
8 | /backend/.env
9 | /backend/src/config/firebase-credential.json
--------------------------------------------------------------------------------
/backend/src/config/firebase-credential.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "",
3 | "project_id": "",
4 | "private_key_id": "",
5 | "private_key": "",
6 | "client_email": "",
7 | "client_id": "",
8 | "auth_uri": "",
9 | "token_uri": "",
10 | "auth_provider_x509_cert_url": "",
11 | "client_x509_cert_url": "",
12 | "universe_domain": ""
13 | }
--------------------------------------------------------------------------------
/backend/src/utils/tryCatch.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 |
3 | const tryCatch = (controller: any) => async (req: express.Request, res: express.Response, next: express.NextFunction) => {
4 | try {
5 | await controller(req, res);
6 | } catch (error) {
7 | return next(error);
8 | }
9 | };
10 |
11 | export default tryCatch;
--------------------------------------------------------------------------------
/backend/src/utils/ErrorResponse.ts:
--------------------------------------------------------------------------------
1 | class ErrorResponse extends Error {
2 | statusCode: number;
3 | errorCode: number;
4 |
5 | constructor(statusCode: number, message: string, errorCode: number) {
6 | super(message);
7 | this.statusCode = statusCode;
8 | this.errorCode = errorCode;
9 | }
10 | }
11 |
12 | export default ErrorResponse;
--------------------------------------------------------------------------------
/frontend/src/components/AllRoutes/Layout/Layout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | height: auto;
4 | background-color: #111827;
5 | display: grid;
6 | grid-template-rows: auto 1fr auto;
7 | grid-template-columns: 100%;
8 | }
9 |
10 | .disabledContainer {
11 | opacity: 0.5;
12 | pointer-events: none;
13 | }
--------------------------------------------------------------------------------
/frontend/src/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | // * ALL REDUCERS
4 | import isDisableReducer from './isDisableReducer';
5 | import errorReducer from './errorReducer';
6 | // * END ALL REDUCERS
7 |
8 | const allReducers = combineReducers({
9 | isDisabled: isDisableReducer,
10 | error: errorReducer
11 | })
12 |
13 | export default allReducers;
--------------------------------------------------------------------------------
/frontend/src/components/Private/Main/Main.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import style from './Main.module.css';
3 |
4 | interface MainProps {
5 | children: ReactNode;
6 | }
7 |
8 | const Main = ({children}: MainProps) => {
9 | return (
10 |
11 | {children}
12 |
13 | )
14 | }
15 |
16 | export default Main;
--------------------------------------------------------------------------------
/frontend/src/assets/spinner-circle-dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/assets/spinner-circle-light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/Private/Footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import style from './Footer.module.css';
2 | import FlexContainer from '../../../components/Private/FlexContainer/FlexContainer';
3 |
4 | const Footer = () => {
5 | return (
6 |
11 | )
12 | }
13 |
14 | export default Footer;
--------------------------------------------------------------------------------
/frontend/src/reducers/isDisableReducer.ts:
--------------------------------------------------------------------------------
1 | type actionType = {
2 | type: string
3 | }
4 |
5 | const isDisableReducer = (state: boolean = false, action: actionType) => {
6 | switch(action.type) {
7 | case 'SET_DISABLE':
8 | return true;
9 | case 'SET_NOT_DISABLE':
10 | return false;
11 | default:
12 | return state;
13 | }
14 | }
15 |
16 | export default isDisableReducer;
--------------------------------------------------------------------------------
/frontend/src/components/Private/FlexContainer/FlexContainer.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import style from './FlexContainer.module.css';
3 |
4 | interface FlexContainerProps {
5 | children: ReactNode;
6 | }
7 |
8 | const FlexContainer = ({children}: FlexContainerProps) => {
9 | return (
10 |
11 | {children}
12 |
13 | )
14 | }
15 |
16 | export default FlexContainer;
--------------------------------------------------------------------------------
/frontend/src/actions/index.ts:
--------------------------------------------------------------------------------
1 | export const setDisable = () => {
2 | return {
3 | type: 'SET_DISABLE'
4 | }
5 | }
6 |
7 | export const setNotDisable = () => {
8 | return {
9 | type: 'SET_NOT_DISABLE'
10 | }
11 | }
12 |
13 | export const hasError = (errorMessage: string) => {
14 | return {
15 | type: 'HAS_ERROR',
16 | errorMessage: errorMessage
17 | }
18 | }
19 |
20 | export const hasNoError = () => {
21 | return {
22 | type: 'HAS_NO_ERROR'
23 | }
24 | }
--------------------------------------------------------------------------------
/backend/src/models/csrfTokenSecretModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema } from 'mongoose';
2 | import { CsrfTokenSecret } from '../interfaces/index.js';
3 |
4 | const csrfTokenSecretSchema: Schema = new Schema({
5 | secret: {
6 | type: String,
7 | required: true
8 | },
9 | user_id: {
10 | type: Schema.Types.ObjectId,
11 | select: false,
12 | ref: 'User'
13 | }
14 | }, {versionKey: false});
15 |
16 | export default mongoose.model('CSRFTokenSecret', csrfTokenSecretSchema);
--------------------------------------------------------------------------------
/frontend/src/components/AllRoutes/CustomInput/CustomInput.tsx:
--------------------------------------------------------------------------------
1 | import { InputHTMLAttributes } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { AllReducers } from '../../../interfaces';
4 | import { Field } from 'formik';
5 |
6 | const CustomInput = ({ ...props }: InputHTMLAttributes) => {
7 | const isDisabled = useSelector((state: AllReducers) => state.isDisabled);
8 |
9 | return (
10 | <>
11 |
12 | >
13 | );
14 | }
15 |
16 | export default CustomInput;
17 |
--------------------------------------------------------------------------------
/frontend/src/components/Private/Header/Header.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | width: 100%;
3 | height: 4rem;
4 | background-color: #1f2937;
5 | border-bottom: 1px solid #323c4b;
6 | }
7 |
8 | .flex_header_container {
9 | display: flex;
10 | justify-content: space-between;
11 | align-items: center;
12 | height: 100%;
13 | }
14 |
15 | .logo_container {
16 | width: 50px;
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | }
21 |
22 | .logo {
23 | width: 32px;
24 | object-fit: cover;
25 | max-width: 32px;
26 | }
27 |
--------------------------------------------------------------------------------
/frontend/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 |
--------------------------------------------------------------------------------
/backend/src/utils/generateRandomUsernameSSO.ts:
--------------------------------------------------------------------------------
1 | function generateRandomUsernameSSO() {
2 | const validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_';
3 | const minLength = 8;
4 | const maxLength = 8;
5 |
6 | let username = '';
7 | const usernameLength = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;
8 |
9 | for (let i = 0; i < usernameLength; i++) {
10 | const randomIndex = Math.floor(Math.random() * validChars.length);
11 | username += validChars.charAt(randomIndex);
12 | }
13 |
14 | return username;
15 | }
16 |
17 | export default generateRandomUsernameSSO;
--------------------------------------------------------------------------------
/frontend/src/components/AllRoutes/Layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import style from './Layout.module.css';
3 | import { useSelector } from 'react-redux';
4 | import { AllReducers } from '../../../interfaces';
5 |
6 | interface LayoutProps {
7 | children: ReactNode;
8 | }
9 |
10 | const Layout = ({children}: LayoutProps) => {
11 | const isDisabled = useSelector((state: AllReducers) => state.isDisabled);
12 |
13 | return (
14 | <>
15 |
16 | {children}
17 |
18 | >
19 | )
20 | }
21 |
22 | export default Layout;
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src",
25 | "tailwind.config.js"
26 | ]
27 | }
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
5 |
6 |
7 | * {
8 | margin: 0px;
9 | padding: 0px;
10 | box-sizing: border-box;
11 | color: white;
12 | text-decoration: none;
13 | -webkit-font-smoothing: antialiased;
14 | -moz-osx-font-smoothing: grayscale;
15 | font-family: 'Inter';
16 | }
17 |
18 | body {
19 | background-color: #111827;
20 | }
21 |
22 | [disabled], .disabled {
23 | opacity: 0.5;
24 | pointer-events: none;
25 | }
26 |
27 | [disabled]:hover {
28 | cursor: initial !important; /* DEFAULT */
29 | opacity: 0.5 !important;
30 | }
--------------------------------------------------------------------------------
/frontend/src/components/AllRoutes/CustomAlert/CustomAlert.tsx:
--------------------------------------------------------------------------------
1 | import Alert from '@mui/material/Alert';
2 | import Stack from '@mui/material/Stack';
3 | import { useSelector } from 'react-redux';
4 | import { AllReducers } from '../../../interfaces';
5 |
6 | const CustomAlert = () => {
7 | const error = useSelector((state: AllReducers) => state.error);
8 |
9 | if(!error.hasError) {
10 | return (
11 | <>>
12 | )
13 | }
14 |
15 | return (
16 | <>
17 |
18 |
19 | { error.errorMessage }
20 |
21 |
22 | >
23 | );
24 | }
25 |
26 | export default CustomAlert;
27 |
--------------------------------------------------------------------------------
/frontend/src/components/AllRoutes/CustomLink/CustomLink.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import { Link, LinkProps } from 'react-router-dom';
3 | import { useSelector, useDispatch } from 'react-redux';
4 | import { AllReducers } from '../../../interfaces';
5 | import { hasNoError } from '../../../actions';
6 |
7 | interface CustomLinkProps extends LinkProps {
8 | children: ReactNode;
9 | }
10 |
11 | const CustomLink = ({ children, ...props }: CustomLinkProps) => {
12 | const isDisabled = useSelector((state: AllReducers) => state.isDisabled);
13 | const dispatch = useDispatch();
14 |
15 | return (
16 | dispatch(hasNoError())} className={isDisabled ? 'disabled' : ''} {...props}>
17 | {children}
18 |
19 | );
20 | }
21 |
22 | export default CustomLink;
23 |
--------------------------------------------------------------------------------
/backend/src/constants/v1AuthenticationJWTTokensSettings.ts:
--------------------------------------------------------------------------------
1 | export const JWT_AUTHENTICATION_TOKEN_EXPIRATION_STRING: string = "60m"; // * HOW LONG THE USER CAN BE AUTHENTICATED WHEN SUCCESS IN LOGIN?
2 |
3 | // * ---------------- FOR EMAIL ----------------
4 | export const JWT_REGISTER_ACCOUNT_ACTIVATION_EXPIRES_IN_STRING: string = "5m"; // * HOW LONG THE EMAIL REGISTER ACCOUNT ACTIVATION LINK TO BE EXPIRED
5 | export const JWT_ACCOUNT_RECOVERY_RESET_PASSWORD_EXPIRES_IN_STRING: string = "5m"; // * HOW LONG THE EMAIL ACCOUNT RECOVERY RESET PASSWORD LINK TO BE EXPIRED
6 |
7 | // * ---------------- FOR MULTI FACTOR AUTHENTICATION LOGIN CODE ----------------
8 | export const JWT_MFA_LOGIN_TOKEN_EXPIRATION_STRING: string = "5m"; // * HOW LONG THE MULTI FACTOR AUTHENTICATION TO BE ENDED BEFORE LOGIN AGAIN?
--------------------------------------------------------------------------------
/frontend/.env.example:
--------------------------------------------------------------------------------
1 | # REACT API - BACKEND
2 | REACT_APP_API=http://localhost:4000
3 |
4 | # FIREBASE SSO CRENDENTIAL
5 | # (You can get firebase credentials in the project settings also but in "General" tab only. Scroll down and click the npm input radio button and copy the firebaseConfig variable value and put it in this .env file)
6 | REACT_APP_SSO_FIREBASE_API_KEY=
7 | REACT_APP_SSO_FIREBASE_AUTH_DOMAIN=
8 | REACT_APP_SSO_FIREBASE_PROJECT_ID=
9 | REACT_APP_SSO_FIREBASE_STORAGE_BUCKET=
10 | REACT_APP_SSO_FIREBASE_MESSAGING_SENDER_ID=
11 | REACT_APP_SSO_FIREBASE_APP_ID=
12 | REACT_APP_SSO_FIREBASE_MEASUREMENT_ID=
13 |
14 | # GOOGLE IDENTITY SERVICES SSO CRENDENTIAL
15 | # (Follow this tutorial on how to create GIS Client ID - [https://youtu.be/HtJKUQXmtok?si=gwlFBdj-vIt-XoSR])
16 | REACT_APP_SSO_GOOGLE_IDENITY_SERVICES_CLIENT_ID=
--------------------------------------------------------------------------------
/frontend/src/reducers/errorReducer.ts:
--------------------------------------------------------------------------------
1 | type stateType = {
2 | hasError: boolean,
3 | errorMessage: string
4 | }
5 |
6 | type actionType = {
7 | type: string,
8 | errorMessage: string
9 | }
10 | const defaultState = {
11 | hasError: false,
12 | errorMessage: ''
13 | }
14 |
15 | const errorReducer = (state: stateType = defaultState, action: actionType) => {
16 | switch(action.type) {
17 | case 'HAS_ERROR':
18 | return {
19 | hasError: true,
20 | errorMessage: action.errorMessage
21 | };
22 | case 'HAS_NO_ERROR':
23 | return {
24 | hasError: false,
25 | errorMessage: ''
26 | };
27 | default:
28 | return state;
29 | }
30 | }
31 |
32 | export default errorReducer;
--------------------------------------------------------------------------------
/frontend/src/pages/Private/Home/Home.tsx:
--------------------------------------------------------------------------------
1 | import style from './Home.module.css';
2 | import Layout from '../../../components/AllRoutes/Layout/Layout';
3 | import Header from '../../../components/Private/Header/Header';
4 | import Main from '../../../components/Private/Main/Main';
5 | import Footer from '../../../components/Private/Footer/Footer';
6 | import FlexContainer from '../../../components/Private/FlexContainer/FlexContainer';
7 |
8 | const Home = () => {
9 | return (
10 |
11 |
12 |
13 |
14 | You're logged in!
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default Home;
--------------------------------------------------------------------------------
/frontend/src/assets/landing_page_icon_3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/Public/FirebaseGoogleSignInButton/FirebaseGoogleSignInButton.module.css:
--------------------------------------------------------------------------------
1 | .sso_button_google {
2 | background: #111827;
3 | border-radius: 5px;
4 | margin-top: 15px;
5 | padding-top: 0.5rem;
6 | padding-bottom: 0.5rem;
7 | padding-left: 0.75rem;
8 | padding-right: 0.75rem;
9 | border: 0 solid #e5e7eb;
10 | border-width: 1px;
11 | border-color: hsl(216 34% 17%);
12 | font-size: .875rem;
13 | line-height: 1.25rem;
14 | height: 2.5rem;
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | gap: 8px;
19 | transition: 0.1s;
20 | font-weight: 500;
21 | width: 100%;
22 | }
23 |
24 | .sso_button_google:hover {
25 | cursor: pointer;
26 | background-color: #1D283A;
27 | }
28 |
29 | .sso_button_google_icon {
30 | width: 18px;
31 | }
--------------------------------------------------------------------------------
/frontend/src/components/Public/GoogleIdentityServices/GoogleIdentityServices.module.css:
--------------------------------------------------------------------------------
1 | .sso_button_google {
2 | background: #111827;
3 | border-radius: 5px;
4 | margin-top: 15px;
5 | padding-top: 0.5rem;
6 | padding-bottom: 0.5rem;
7 | padding-left: 0.75rem;
8 | padding-right: 0.75rem;
9 | border: 0 solid #e5e7eb;
10 | border-width: 1px;
11 | border-color: hsl(216 34% 17%);
12 | font-size: .875rem;
13 | line-height: 1.25rem;
14 | height: 2.5rem;
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | gap: 8px;
19 | transition: 0.1s;
20 | font-weight: 500;
21 | width: 100%;
22 |
23 | }
24 |
25 | .sso_button_google:hover {
26 | cursor: pointer;
27 | background-color: #1D283A;
28 | }
29 |
30 | .sso_button_google_icon {
31 | width: 18px;
32 | }
--------------------------------------------------------------------------------
/frontend/src/components/Public/FirebaseFacebookSignInButton/FirebaseFacebookSignInButton.module.css:
--------------------------------------------------------------------------------
1 | .sso_button_facebook {
2 | background: #111827;
3 | border-radius: 5px;
4 | margin-top: 15px;
5 | padding-top: 0.5rem;
6 | padding-bottom: 0.5rem;
7 | padding-left: 0.75rem;
8 | padding-right: 0.75rem;
9 | border: 0 solid #e5e7eb;
10 | border-width: 1px;
11 | border-color: hsl(216 34% 17%);
12 | font-size: .875rem;
13 | line-height: 1.25rem;
14 | height: 2.5rem;
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | gap: 8px;
19 | transition: 0.1s;
20 | font-weight: 500;
21 | width: 100%;
22 |
23 | }
24 |
25 | .sso_button_facebook:hover {
26 | cursor: pointer;
27 | background-color: #1D283A;
28 | }
29 |
30 | .sso_button_facebook_icon {
31 | width: 18px;
32 | }
--------------------------------------------------------------------------------
/frontend/src/components/AllRoutes/CustomButton/CustomButton.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, ButtonHTMLAttributes, forwardRef } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { AllReducers } from '../../../interfaces';
4 | import SpinnerCircleDark from '../../../assets/spinner-circle-dark.svg';
5 |
6 | interface CustomButtonProps extends ButtonHTMLAttributes {
7 | children: ReactNode;
8 | }
9 |
10 | const CustomButton = forwardRef(({ children, ...props }, ref) => {
11 | const isDisabled = useSelector((state: AllReducers) => state.isDisabled);
12 |
13 | return (
14 |
18 | );
19 | }
20 | );
21 |
22 | export default CustomButton;
23 |
--------------------------------------------------------------------------------
/backend/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import { JwtPayload } from "jsonwebtoken";
2 |
3 | export type Error = {
4 | name?: string,
5 | errors?: any,
6 | statusCode: number,
7 | message: string,
8 | errorCode: number
9 | }
10 |
11 | export type RETURN_ERROR = {
12 | message: string,
13 | errorCode: number
14 | }
15 |
16 | export type MFA_LOGIN_TOKEN = JwtPayload & {
17 | username: string;
18 | profilePicture: string;
19 | hasGoogleAuthentication: boolean;
20 | };
21 |
22 | export type SSO_GOOGLE_IDENTITY_SERVICES_TOKEN_DECODED = {
23 | email: string;
24 | name: string;
25 | picture: string;
26 | }
27 |
28 | export type SSO_FIREBASE_FACEBOOK_TOKEN_DECODED = {
29 | email: string;
30 | name: string;
31 | picture: string;
32 | user_id: string;
33 | }
34 |
35 | export type SSO_FIREBASE_GOOGLE_TOKEN_DECODED = {
36 | email: string;
37 | name: string;
38 | picture: string;
39 | email_verified: boolean;
40 | user_id: string;
41 | }
--------------------------------------------------------------------------------
/frontend/src/config/firebase-config.ts:
--------------------------------------------------------------------------------
1 | import { initializeApp } from 'firebase/app';
2 | import { getAuth } from "firebase/auth";
3 |
4 | type firebaseConfigType = {
5 | readonly apiKey: string,
6 | readonly authDomain: string,
7 | readonly projectId: string,
8 | readonly storageBucket: string,
9 | readonly messagingSenderId: string,
10 | readonly appId: string
11 | }
12 |
13 | const firebaseConfig: firebaseConfigType = {
14 | apiKey: process.env.REACT_APP_SSO_FIREBASE_API_KEY || '',
15 | authDomain: process.env.REACT_APP_SSO_FIREBASE_AUTH_DOMAIN || '',
16 | projectId: process.env.REACT_APP_SSO_FIREBASE_PROJECT_ID || '',
17 | storageBucket: process.env.REACT_APP_SSO_FIREBASE_STORAGE_BUCKET || '',
18 | messagingSenderId: process.env.REACT_APP_SSO_FIREBASE_MESSAGING_SENDER_ID || '',
19 | appId: process.env.REACT_APP_SSO_FIREBASE_APP_ID || '',
20 | };
21 |
22 | const app = initializeApp(firebaseConfig);
23 |
24 | export const authentication = getAuth(app);
--------------------------------------------------------------------------------
/backend/src/models/googleAuthenticatorModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema } from 'mongoose';
2 | import { GoogleAuthenticator } from '../interfaces/index.js';
3 |
4 | const googleAuthenticationSchema: Schema = new Schema({
5 | secret: {
6 | type: String,
7 | select: false,
8 | required: true
9 | },
10 | encoding: {
11 | type: String,
12 | select: false,
13 | required: true
14 | },
15 | qr_code: {
16 | type: String,
17 | select: false,
18 | required: true
19 | },
20 | otpauth_url: {
21 | type: String,
22 | select: false,
23 | required: true
24 | },
25 | isActivated: {
26 | type: 'boolean',
27 | required: true,
28 | default: false
29 | },
30 | user_id: {
31 | type: Schema.Types.ObjectId,
32 | select: false,
33 | ref: 'User'
34 | }
35 | }, { timestamps: true, versionKey: false });
36 |
37 | export default mongoose.model('GoogleAuthentication', googleAuthenticationSchema);
--------------------------------------------------------------------------------
/backend/src/constants/v1AuthenticationCookiesSettings.ts:
--------------------------------------------------------------------------------
1 | // * CUSTOMIZE COOKIES NAMES
2 | export const COOKIE_AUTHENTICATION_TOKEN_NAME: string = "AUTH_TOKEN"; // * NAME OF THE COOKIE AUTHENTICATION TOKEN (AUTHENTICATED USER COOKIE)
3 | export const COOKIE_CSRF_TOKEN_NAME: string = "XSRF_TOKEN"; // * NAME OF THE COOKIE CSRF TOKEN (AUTHENTICATED USER CSRF TOKEN OR PUBLIC CSRF TOKEN (WHEN NOT LOGGED IN))
4 | export const COOKIE_MFA_TOKEN_NAME: string = "MFA_TOKEN"; // * NAME OF THE COOKIE USER MULTI FACTOR AUTHENTICATION TOKEN (AUTHENTICATED USER)
5 |
6 | // * CUSTOMIZE COOKIES EXPIRATIONS
7 | export const COOKIE_AUTHENTICATION_TOKEN_EXPIRATION: number = 60 * 1000 * 60; // * HOW LONG THE USER CAN BE AUTHENTICATED WHEN SUCCESS IN LOGIN?
8 | export const COOKIE_PUBLIC_CSRF_TOKEN_EXPIRATION: number = 60 * 1000 * 60; // * HOW LONG THE PUBLIC CSRF TO BE EXPIRED
9 | export const COOKIE_MFA_LOGIN_TOKEN_EXPIRATION: number = 60 * 1000 * 5; // * HOW LONG THE MULTI FACTOR AUTHENTICATION TO BE ENDED BEFORE LOGIN AGAIN?
--------------------------------------------------------------------------------
/backend/.env.example:
--------------------------------------------------------------------------------
1 | # TYPE OF NODE ENVIRONMENT
2 | NODE_ENV=DEVELOPMENT
3 |
4 | # PORT OF SERVER
5 | PORT=4000
6 |
7 | # VERSION OF NODE
8 | NODE_VERSION=18.16.0
9 |
10 | # REACT URL - FRONTEND
11 | REACT_URL=http://localhost:3000
12 |
13 | # MONGO DATABASE URI
14 | MONGO_DB_URI=
15 | MONGO_DB_URI_LIMITER=
16 |
17 | # SECRETS
18 | # (Create secret using this code "node require('crypto').randomBytes(64).toString('hex');")
19 | AUTHENTICATION_TOKEN_SECRET=
20 | ACCOUNT_ACTIVATION_TOKEN_SECRET=
21 | ACCOUNT_RECOVERY_RESET_PASSWORD_TOKEN_SECRET=
22 | ACCOUNT_RECOVERY_RESET_PASSWORD_CSRF_TOKEN_SECRET=
23 | MFA_TOKEN_SECRET=
24 | PUBLIC_CSRF_TOKEN_SECRET=
25 |
26 | # SMTP CONFIGURATION. USED BREVO FORMERLY SENDINBLUE EMAIL SERVICES
27 | SMTP_HOST=
28 | SMTP_PORT=
29 | SMTP_USER=
30 | SMTP_PASSWORD=
31 | EMAIL_FROM=
32 |
33 | # GOOGLE AUTHENTICATOR SECRET
34 | GOOGLE_AUTHENTICATOR_NAME=MERN-Auth
35 |
36 | # SSO GOOGLE IDENTITY SERVICES
37 | # (Follow this tutorial on how to create GIS Client ID - [https://youtu.be/HtJKUQXmtok?si=gwlFBdj-vIt-XoSR])
38 | GOOGLE_IDENITY_SERVICES_CLIENT_ID=
--------------------------------------------------------------------------------
/backend/Globals.d.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | namespace NodeJS {
3 | interface ProcessEnv {
4 | NODE_ENV: "DEVELOPMENT" | "PRODUCTION";
5 |
6 | PORT: string;
7 |
8 | REACT_URL: string;
9 |
10 | MONGO_DB_URI: string;
11 | MONGO_DB_URI_LIMITER: string;
12 |
13 | AUTHENTICATION_TOKEN_SECRET: string;
14 | ACCOUNT_ACTIVATION_TOKEN_SECRET: string;
15 | ACCOUNT_RECOVERY_RESET_PASSWORD_TOKEN_SECRET: string;
16 | ACCOUNT_RECOVERY_RESET_PASSWORD_CSRF_TOKEN_SECRET: string;
17 | MFA_TOKEN_SECRET: string;
18 | PUBLIC_CSRF_TOKEN_SECRET: string;
19 |
20 | SMTP_HOST: string;
21 | SMTP_PORT: string;
22 | SMTP_USER: string;
23 | SMTP_PASSWORD: string;
24 | EMAIL_FROM: string;
25 |
26 | GOOGLE_AUTHENTICATOR_NAME: string;
27 | }
28 | }
29 | }
30 |
31 | declare module "*.json" {
32 | const value: any;
33 | export default value;
34 | }
35 |
36 | export {}
--------------------------------------------------------------------------------
/backend/src/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | import { Document } from 'mongoose';
2 |
3 | export interface User extends Document {
4 | username: string;
5 | email: string;
6 | password: string;
7 | forgotPassword: boolean;
8 | isSSO: boolean;
9 | verificationCodeLogin: string;
10 | googleAuthenticator: string | any;
11 | csrfTokenSecret: string | any;
12 | profile: string | any;
13 | matchPasswords(password: string): Promise;
14 | matchVerificationCodeLogin(verificationCodeLogin: string): Promise;
15 | social_id: string;
16 | }
17 |
18 | export interface Profile extends Document {
19 | fullName: string;
20 | profilePicture: string;
21 | user_id: string | any;
22 | }
23 |
24 | export interface GoogleAuthenticator extends Document {
25 | secret: string;
26 | encoding: string;
27 | qr_code: string;
28 | otpauth_url: string;
29 | isActivated: boolean;
30 | user_id: string | any;
31 | }
32 |
33 | export interface CsrfTokenSecret extends Document {
34 | secret: string;
35 | user_id: string | any;
36 | }
37 |
38 | export interface ValidationResult {
39 | error: any;
40 | value: any;
41 | }
42 |
--------------------------------------------------------------------------------
/backend/src/middlewares/errorHandler.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import ErrorResponse from "../utils/ErrorResponse.js"; // * UTILITY
3 | import * as errorCodes from '../constants/v1AuthenticationErrorCodes.js'; // * CONSTANTS
4 | import { Error } from '../types/index.js';
5 |
6 | function errorHandler (error: Error, req: express.Request, res: express.Response, next: express.NextFunction) {
7 | // * THIS IS ERROR FROM THE MONGOOSE MODEL VALIDATION USER INPUT
8 | if (error.name === "ValidationError") {
9 | const message: any = Object.values(error.errors).map((val: any) => val.message);
10 | error = new ErrorResponse(400, message, errorCodes.MONGOOSE_VALIDATION_ERROR);
11 | }
12 |
13 | if (process.env["NODE_ENV"] as string === "PRODUCTION") {
14 | return res.status(500).json({
15 | message: "There is something problem on the server. Please try again later.",
16 | errorCode: errorCodes.SERVER_ERROR
17 | });
18 | }
19 |
20 | return res.status(error.statusCode || 500).json({
21 | message: error.message || "There is something problem on the server. Please try again later.",
22 | errorCode: error.errorCode || errorCodes.SERVER_ERROR
23 | });
24 | }
25 |
26 | export default errorHandler;
--------------------------------------------------------------------------------
/backend/src/models/profileModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema } from 'mongoose';
2 | import he from 'he';
3 | import { Profile } from '../interfaces/index.js';
4 | import * as userSettings from '../constants/v1AuthenticationUserSettings.js'; // * ALL USER SETTINGS
5 |
6 | const profileSchema: Schema = new Schema({
7 | fullName: {
8 | type: String,
9 | trim: true,
10 | required: [true, 'Full Name is required'],
11 | maxlength: [50, 'Full Name must not exceed 50 characters'],
12 | match: [/^[A-Za-z.\s]+$/, 'Full Name must contain letters and dots only'],
13 | validate: [
14 | {
15 | validator: function(value: string) {
16 | const sanitizedValue = he.escape(value);
17 | return sanitizedValue === value;
18 | },
19 | message: 'Full Name contains potentially unsafe characters or invalid characters',
20 | },
21 | ],
22 | },
23 | profilePicture: {
24 | type: String,
25 | required: true,
26 | default: userSettings.DEFAULT_PROFILE_PICTURE
27 | },
28 | user_id: {
29 | type: Schema.Types.ObjectId,
30 | select: false,
31 | ref: 'User'
32 | }
33 | },{ timestamps: true, versionKey: false })
34 |
35 | export default mongoose.model('Profile', profileSchema);
--------------------------------------------------------------------------------
/backend/src/utils/generateRandomPasswordSSO.ts:
--------------------------------------------------------------------------------
1 | function generateRandomPasswordSSO() {
2 | const uppercaseLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
3 | const lowercaseLetters = 'abcdefghijklmnopqrstuvwxyz';
4 | const digits = '0123456789';
5 | const specialCharacters = '!@#$%^&*()_+~`|}{[]\:;?><,./-=';
6 |
7 | const minLength = 12;
8 |
9 | // * Create an array containing all the required character types
10 | const requiredCharacters = [
11 | uppercaseLetters,
12 | lowercaseLetters,
13 | digits,
14 | specialCharacters
15 | ];
16 |
17 | let password = '';
18 |
19 | // * Add one character from each required character type
20 | requiredCharacters.forEach((charType) => {
21 | const randomIndex = Math.floor(Math.random() * charType.length);
22 | password += charType.charAt(randomIndex);
23 | });
24 |
25 | // * Add remaining characters randomly
26 | const remainingLength = minLength - requiredCharacters.length;
27 |
28 | for (let i = 0; i < remainingLength; i++) {
29 | const charType = requiredCharacters[Math.floor(Math.random() * requiredCharacters.length)];
30 | const randomIndex = Math.floor(Math.random() * charType.length);
31 | password += charType.charAt(randomIndex);
32 | }
33 |
34 | return password;
35 | }
36 |
37 | export default generateRandomPasswordSSO;
--------------------------------------------------------------------------------
/backend/src/utils/sendEmail.ts:
--------------------------------------------------------------------------------
1 | import nodemailer, { TransportOptions } from "nodemailer";
2 |
3 | type SendEmailOptions = {
4 | to: string;
5 | subject: string;
6 | text: string;
7 | html: string;
8 | };
9 |
10 | const sendEmail = async ({ to, subject, text, html }: SendEmailOptions) => {
11 | const transporter = nodemailer.createTransport({
12 | // * Cast the options to the TransportOptions type
13 | host: process.env["SMTP_HOST"] as string,
14 | port: process.env["SMTP_PORT"] as string,
15 | secure: process.env["NODE_ENV"] as string === "PRODUCTION" ? true : false as boolean,
16 | auth: {
17 | user: process.env["SMTP_USER"] as string,
18 | pass: process.env["SMTP_PASSWORD"] as string,
19 | }
20 | } as TransportOptions); // * Cast the object to TransportOptions
21 |
22 | const emailOptions = {
23 | from: `MERN <${process.env["EMAIL_FROM"] as string}>`,
24 | to,
25 | subject,
26 | text,
27 | html,
28 | };
29 |
30 | // * Sending email activation account
31 | transporter.sendMail(emailOptions, (error: any, info: any) => {
32 | if (error) {
33 | console.log({
34 | fileName: 'sendEmail.ts',
35 | errorDescription: 'There is something problem on the sending the activation link to the user via email.',
36 | errorLocation: 'sendEmail',
37 | error: error
38 | });
39 | }
40 | });
41 | };
42 |
43 | export default sendEmail;
44 |
--------------------------------------------------------------------------------
/frontend/src/routes/MFARoutes.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Navigate, Outlet } from 'react-router-dom';
3 | import { isMFAMode } from '../helpers/auth'; // Import your authentication helper
4 | import Loading from '../components/AllRoutes/Loading/Loading';
5 |
6 | const PublicRoutes = () => {
7 | const [loading, setLoading] = useState(true);
8 | const [mfaMode, setMFAMode] = useState(false);
9 | const [isErrConnectionRefused, setIsErrConnectionRefused] = useState(false);
10 | const [user, setUser] = useState({});
11 |
12 | useEffect(() => {
13 | const checkIfMFAMode = async () => {
14 | try {
15 | const result: any = await isMFAMode(); // Assuming this function returns a promise
16 | setMFAMode(result.status === 'MFA-Mode' ? true : false);
17 | setUser(result.status === 'MFA-Mode' ? result.user : {});
18 | setLoading(false);
19 | } catch (error) {
20 | // Handle any error that occurred during authentication
21 | setIsErrConnectionRefused(true);
22 | }
23 | };
24 |
25 | checkIfMFAMode();
26 | // eslint-disable-next-line react-hooks/exhaustive-deps
27 | }, []);
28 |
29 | if(isErrConnectionRefused) {
30 | return
31 | }
32 |
33 | if (loading) {
34 | return ; // or any loading indicator/component
35 | }
36 |
37 | if (mfaMode) {
38 | return ;
39 | }
40 |
41 | return
42 | };
43 |
44 | export default PublicRoutes;
--------------------------------------------------------------------------------
/frontend/src/pages/Public/AccountActivation/AccountActivation.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | width: 100%;
3 | height: 90px;
4 | display: flex;
5 | justify-content: space-between;
6 | padding: 0px 3rem;
7 | align-items: center;
8 | }
9 |
10 | .logo_container {
11 | font-size: .875rem;
12 | line-height: 1.25rem;
13 | padding: 0px 10px;
14 | color: #9ba2ae;
15 | transition: 0.1s;
16 | width: 50px;
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | }
21 |
22 | .logo {
23 | width: 40px;
24 | object-fit: cover;
25 | }
26 |
27 | .nav_links {
28 | height: 100%;
29 | display: flex;
30 | justify-content: space-around;
31 | align-items: center;
32 | }
33 |
34 | .account_activation {
35 | width: 350px;
36 | height: 500px;
37 | flex-direction: column;
38 | display: flex;
39 | }
40 |
41 | .main {
42 | margin: 0 auto;
43 | }
44 |
45 | .account_activation_title {
46 | text-align: center;
47 | font-family: 'Inter';
48 | font-style: normal;
49 | font-weight: 600;
50 | font-size: 1.5rem;
51 | color: #e1e7ef;
52 | }
53 |
54 | .account_activation_subtitle {
55 | text-align: center;
56 | font-family: 'Inter';
57 | font-style: normal;
58 | font-weight: 400;
59 | font-size: .875rem;
60 | line-height: 1.25rem;
61 | color: #7A889D;
62 | margin-top: 15px;
63 | margin-bottom: 20px;
64 | }
65 |
66 | @media screen and (max-width: 768px) {
67 | .header {
68 | padding: 0px 1.5rem;
69 | }
70 | }
--------------------------------------------------------------------------------
/frontend/src/routes/PrivateRoutes.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Navigate, Outlet } from 'react-router-dom';
3 | import { isAuthenticated } from '../helpers/auth'; // Import your authentication helper
4 | import Loading from '../components/AllRoutes/Loading/Loading';
5 |
6 | type resultType = {
7 | status: 'ok' | 'fail' | 'ERR_CONNECTION_REFUSED',
8 | user?: object
9 | }
10 |
11 | const PrivateRoutes = () => {
12 | const [loading, setLoading] = useState(true);
13 | const [authenticated, setAuthenticated] = useState(false);
14 | const [isErrConnectionRefused, setIsErrConnectionRefused] = useState(false);
15 | const [user, setUser] = useState({});
16 |
17 | useEffect(() => {
18 | const checkAuthentication = async () => {
19 | try {
20 | const result: resultType = await isAuthenticated();
21 | setAuthenticated(result.status === 'ok' ? true : false);
22 | setUser(result.user ? result.user : {status: 'fail'});
23 | setLoading(false);
24 | } catch (error) {
25 | // Handle any error that occurred during authentication
26 | setIsErrConnectionRefused(true);
27 | }
28 | };
29 |
30 | checkAuthentication();
31 | }, []);
32 |
33 | if(isErrConnectionRefused) {
34 | return
35 | }
36 |
37 | if (loading) {
38 | return ; // or any loading indicator/component
39 | }
40 |
41 | if (!authenticated) {
42 | return ;
43 | }
44 |
45 | return ;
46 | };
47 |
48 | export default PrivateRoutes;
--------------------------------------------------------------------------------
/frontend/src/helpers/auth.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | type isAuthenticatedPromiseType = {
4 | status: 'ok' | 'fail' | 'ERR_CONNECTION_REFUSED',
5 | user?: object
6 | }
7 |
8 | type isMFAModePromiseType = {
9 | status: 'MFA-Mode' | 'ok' | 'fail' | 'ERR_CONNECTION_REFUSED' | any,
10 | user?: object
11 | }
12 |
13 | export const isAuthenticated = async (): Promise => {
14 | try {
15 | const response = await axios.get(`${process.env.REACT_APP_API}/api/v1/authentication/user`);
16 | if (response.status === 200 && response.data.status === 'ok') return {status: 'ok', user: response.data.user};
17 | return {status: 'fail'};
18 | } catch (error: any) {
19 | if(error.response.data.message === 'You are unauthorized user.' && error.response.data.errorCode === 300) {
20 | return {status: 'fail'};
21 | }else {
22 | return {status: 'ERR_CONNECTION_REFUSED'};
23 | }
24 | }
25 | };
26 |
27 | export const isMFAMode = async (): Promise => {
28 | try {
29 | const response = await axios.get(`${process.env.REACT_APP_API}/api/v1/authentication/user`);
30 | if (response.status === 200 && response.data.status === 'MFA-Mode') return {status: response.data.status, user: response.data.user};
31 | if (response.status === 200 && response.data.status === 'ok') return {status: response.data.status, user: response.data.user};
32 | return {status: 'fail'};
33 | } catch (error: any) {
34 | if(error.response.data.message === 'You are unauthorized user.' && error.response.data.errorCode === 300) {
35 | return {status: 'fail'};
36 | }else {
37 | return {status: 'ERR_CONNECTION_REFUSED'};
38 | }
39 | }
40 | };
--------------------------------------------------------------------------------
/frontend/src/routes/PublicRoutes.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Navigate, Outlet } from 'react-router-dom';
3 | import { isMFAMode } from '../helpers/auth'; // Import your authentication helper
4 | import Loading from '../components/AllRoutes/Loading/Loading';
5 |
6 | type isMFAModeResultType = {
7 | status: 'MFA-Mode' | 'ok' | 'fail' | 'ERR_CONNECTION_REFUSED',
8 | user?: object
9 | }
10 |
11 | const PublicRoutes = () => {
12 | const [loading, setLoading] = useState(true);
13 | const [authenticated, setAuthenticated] = useState(false);
14 | const [isErrConnectionRefused, setIsErrConnectionRefused] = useState(false);
15 | const [mfa, setMFA] = useState(false);
16 |
17 | useEffect(() => {
18 | const checkAuthentication = async () => {
19 | try {
20 | const isMFAModeResult: isMFAModeResultType = await isMFAMode(); // Assuming this function returns a promise
21 | setMFA(isMFAModeResult.status === 'MFA-Mode' ? true : false);
22 | setAuthenticated(isMFAModeResult.status === 'ok' ? true : false);
23 | setLoading(false);
24 | } catch (error) {
25 | // Handle any error that occurred during authentication
26 | setIsErrConnectionRefused(true);
27 | }
28 | };
29 |
30 | checkAuthentication();
31 | // eslint-disable-next-line react-hooks/exhaustive-deps
32 | }, []);
33 |
34 | if(isErrConnectionRefused === true) {
35 | return
36 | }
37 |
38 | if (loading) {
39 | return ; // or any loading indicator/component
40 | }
41 |
42 | if (!authenticated && mfa) {
43 | return
44 | }
45 |
46 | if(!authenticated && !mfa) {
47 | return ;
48 | }
49 |
50 | return
51 | };
52 |
53 | export default PublicRoutes;
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mern-auth-boilerplate-typescript",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/react": "^11.11.1",
7 | "@emotion/styled": "^11.11.0",
8 | "@headlessui/react": "^1.7.15",
9 | "@heroicons/react": "^2.0.18",
10 | "@mui/material": "^5.13.6",
11 | "@react-oauth/google": "^0.11.0",
12 | "@reduxjs/toolkit": "^1.9.5",
13 | "@testing-library/jest-dom": "^5.16.5",
14 | "@testing-library/react": "^13.4.0",
15 | "@testing-library/user-event": "^13.5.0",
16 | "@types/jest": "^27.5.2",
17 | "@types/node": "^16.18.38",
18 | "@types/react": "^18.2.14",
19 | "@types/react-dom": "^18.2.6",
20 | "axios": "^1.4.0",
21 | "dompurify": "^3.0.3",
22 | "firebase": "^9.23.0",
23 | "formik": "^2.4.2",
24 | "he": "^1.2.0",
25 | "react": "^18.2.0",
26 | "react-dom": "^18.2.0",
27 | "react-redux": "^8.1.1",
28 | "react-router-dom": "^6.14.0",
29 | "react-scripts": "^5.0.1",
30 | "redux": "^4.2.1",
31 | "typescript": "^4.9.5",
32 | "web-vitals": "^2.1.4",
33 | "yup": "^1.2.0"
34 | },
35 | "scripts": {
36 | "start": "react-scripts start",
37 | "build": "react-scripts build",
38 | "test": "react-scripts test",
39 | "eject": "react-scripts eject"
40 | },
41 | "eslintConfig": {
42 | "extends": [
43 | "react-app",
44 | "react-app/jest"
45 | ]
46 | },
47 | "browserslist": {
48 | "production": [
49 | ">0.2%",
50 | "not dead",
51 | "not op_mini all"
52 | ],
53 | "development": [
54 | "last 1 chrome version",
55 | "last 1 firefox version",
56 | "last 1 safari version"
57 | ]
58 | },
59 | "devDependencies": {
60 | "@types/dompurify": "^3.0.2",
61 | "@types/he": "^1.2.0",
62 | "tailwindcss": "^3.3.2"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server",
6 | "types": "dist/server",
7 | "type": "module",
8 | "scripts": {
9 | "test": "echo \"Error: no test specified\" && exit 1",
10 | "start": "nodemon",
11 | "dev": "nodemon",
12 | "build": "tsc",
13 | "production": "node dist/server.js"
14 | },
15 | "keywords": [],
16 | "author": "",
17 | "license": "ISC",
18 | "dependencies": {
19 | "argon2": "^0.27.1",
20 | "chalk": "^5.2.0",
21 | "colors": "^1.4.0",
22 | "compression": "^1.7.4",
23 | "cookie-parser": "^1.4.6",
24 | "cors": "^2.8.5",
25 | "csrf": "^3.1.0",
26 | "dotenv": "^16.3.1",
27 | "express": "^4.18.2",
28 | "express-mongo-sanitize": "^2.2.0",
29 | "express-rate-limit": "^6.7.0",
30 | "firebase-admin": "^11.10.1",
31 | "google-auth-library": "^8.9.0",
32 | "he": "^1.2.0",
33 | "helmet": "^7.0.0",
34 | "joi": "^17.9.2",
35 | "jsonwebtoken": "^9.0.0",
36 | "lodash": "^4.17.21",
37 | "mongoose": "^6.8.3",
38 | "nodemailer": "^6.9.2",
39 | "qrcode": "^1.5.3",
40 | "rate-limit-mongo": "^2.3.2",
41 | "speakeasy": "^2.0.0",
42 | "xss": "^1.0.14"
43 | },
44 | "devDependencies": {
45 | "@types/argon2": "^0.15.0",
46 | "@types/compression": "^1.7.2",
47 | "@types/cookie-parser": "^1.4.3",
48 | "@types/cors": "^2.8.13",
49 | "@types/csrf": "^3.1.0",
50 | "@types/dotenv": "^8.2.0",
51 | "@types/express": "^4.17.17",
52 | "@types/express-mongo-sanitize": "^2.1.0",
53 | "@types/express-rate-limit": "^6.0.0",
54 | "@types/he": "^1.2.0",
55 | "@types/helmet": "^4.0.0",
56 | "@types/joi": "^17.2.3",
57 | "@types/jsonwebtoken": "^9.0.2",
58 | "@types/lodash": "^4.14.195",
59 | "@types/mongoose": "^5.11.97",
60 | "@types/node": "^20.4.2",
61 | "@types/nodemailer": "^6.4.8",
62 | "@types/qrcode": "^1.5.0",
63 | "@types/speakeasy": "^2.0.7",
64 | "nodemon": "^2.0.20",
65 | "ts-node": "^10.9.1",
66 | "typescript": "^5.1.3"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/frontend/src/components/Private/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from 'react-router-dom';
2 | import axios from 'axios';
3 | import style from './Header.module.css';
4 | import logo from '../../../assets/logo-header.png';
5 | import HeaderDropdown from '../../../components/Private/HeaderDropdown/HeaderDropdown';
6 | import FlexContainer from '../../../components/Private/FlexContainer/FlexContainer';
7 |
8 | import { useDispatch } from 'react-redux';
9 | import { setDisable, setNotDisable } from '../../../actions';
10 |
11 | const Header = () => {
12 | const navigate = useNavigate();
13 | const dispatch = useDispatch();
14 |
15 | function handleLogout() {
16 | dispatch(setDisable());
17 | axios.post(`${process.env.REACT_APP_API}/api/v1/authentication/logout`)
18 | .then((response) => {
19 | if (response.status === 200 && response.data.status === 'ok') {
20 | dispatch(setNotDisable());
21 | navigate('/login');
22 | }
23 | })
24 | .catch(function (error) {
25 | dispatch(setNotDisable());
26 | navigate('/login');
27 | });
28 | }
29 |
30 | function handleDeleteUser() {
31 | dispatch(setDisable());
32 | axios.delete(`${process.env.REACT_APP_API}/api/v1/authentication/user`)
33 | .then((response) => {
34 | if (response.status === 200 && response.data.status === 'ok') {
35 | dispatch(setNotDisable());
36 | navigate('/login');
37 | }
38 | })
39 | .catch(function (error) {
40 | dispatch(setNotDisable());
41 | navigate('/login');
42 | });
43 | }
44 |
45 | return (
46 |
47 |
48 |
49 |
50 |

51 |
52 |
{ handleLogout() }} handleDeleteUser={() => handleDeleteUser()}/>
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export default Header;
--------------------------------------------------------------------------------
/backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Added */
4 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
5 |
6 | /* Basic Options */
7 | "target": "ESNext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
8 | "module": "NodeNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
9 | "outDir": "dist", /* Redirect output structure to the directory. */
10 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
11 | "sourceMap": true, /* Generates corresponding '.map' file. */
12 | "removeComments": true, /* Do not emit comments to output. */
13 |
14 | /* Strict Type-Checking Options */
15 | "strict": true, /* Enable all strict type-checking options. */
16 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
17 | "strictNullChecks": true, /* Enable strict null checks. */
18 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
19 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
20 | "strictFunctionTypes": true, /* Enable strict checking of function types. */
21 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
22 |
23 | /* Additional Checks */
24 | "noUnusedLocals": true, /* Report errors on unused locals. */
25 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
26 |
27 | /* Module Resolution Options */
28 | "moduleResolution": "NodeNext", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
29 | "baseUrl": "src", /* Base directory to resolve non-absolute module names. */
30 |
31 | "resolveJsonModule": true
32 | },
33 | "include": ["src/**/*", "*.d.ts"]
34 | }
--------------------------------------------------------------------------------
/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client';
2 | import {BrowserRouter as Router, Route, Routes, Navigate} from 'react-router-dom';
3 |
4 | import App from './App';
5 | import Home from './pages/Private/Home/Home';
6 | import Login from './pages/Public/Login/Login';
7 | import LoginVerificationCode from './pages/MFA/LoginVerificationCode/LoginVerificationCode';
8 | import Register from './pages/Public/Register/Register';
9 | import AccountActivation from './pages/Public/AccountActivation/AccountActivation';
10 | import ForgotPassword from './pages/Public/ForgotPassword/ForgotPassword';
11 | import ResetPassword from './pages/Public/ResetPassword/ResetPassword';
12 |
13 | import MFARoutes from "./routes/MFARoutes";
14 | import PublicRoutes from "./routes/PublicRoutes";
15 | import PrivateRoutes from "./routes/PrivateRoutes";
16 |
17 | import { GoogleOAuthProvider } from '@react-oauth/google';
18 |
19 | import reducers from './reducers';
20 | import { Provider } from 'react-redux';
21 | import { configureStore } from '@reduxjs/toolkit';
22 |
23 | const store = configureStore({reducer: reducers});
24 |
25 | const root = ReactDOM.createRoot(
26 | document.getElementById('root') as HTMLElement
27 | );
28 |
29 | root.render(
30 |
31 |
32 |
33 |
34 | {/* -------------- LANDING PAGE ROUTE ------------ */}
35 | }/>
36 |
37 | {/* -------------- MULTI FACTOR AUTHENTICATION ROUTES ------------ */}
38 | }>
39 | }/>
40 |
41 |
42 | {/* -------------- PUBLIC ROUTES ------------ */}
43 | }>
44 | }/>
45 | }/>
46 | }/>
47 | }/>
48 | }/>
49 |
50 |
51 | {/* -------------- PRIVATE ROUTES REQUIRES JWT AUTHENTICATION TOKEN ------------ */}
52 | }>
53 | } />
54 |
55 |
56 | } />
57 |
58 |
59 |
60 |
61 | );
62 |
--------------------------------------------------------------------------------
/frontend/src/components/Public/GoogleIdentityServices/GoogleIdentityServices.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import DOMPurify from 'dompurify'; // FOR SANITIZING USER INPUT TO PREVENT XSS ATTACKS BEFORE SENDING TO THE BACKEND
4 | import axios from 'axios';
5 |
6 | import { useSelector, useDispatch } from 'react-redux';
7 | import { setDisable, setNotDisable, hasError, hasNoError } from '../../../actions';
8 | import { AllReducers } from '../../../interfaces';
9 |
10 | interface GoogleIdentityServicesProps {
11 | addButton: boolean;
12 | addPrompt: boolean;
13 | text: 'signin_with' | 'signup_with';
14 | }
15 |
16 | const GoogleIdentityServices = ({addButton, addPrompt, text}: GoogleIdentityServicesProps) => {
17 | const navigate = useNavigate();
18 | const isDisabled = useSelector((state: AllReducers) => state.isDisabled);
19 | const error = useSelector((state: AllReducers) => state.error);
20 | const dispatch = useDispatch();
21 |
22 | function googleIdentityServices(response: {credential: string}) {
23 | if(!isDisabled) {
24 | const sanitizedToken = DOMPurify.sanitize(response.credential);
25 | dispatch(setDisable());
26 | axios.post(`${process.env.REACT_APP_API}/api/v1/authentication/sso/${text === 'signin_with' ? 'sign-in' : 'sign-up'}/google-identity-services`, {
27 | token: sanitizedToken
28 | })
29 | .then((response) => {
30 | if(response.status === 200 && response.data.status === 'ok') {
31 | dispatch(setNotDisable());
32 | if(error.hasError) {
33 | dispatch(hasNoError());
34 | }
35 | navigate('/home');
36 | }
37 | })
38 | .catch(function (error) {
39 | dispatch(setNotDisable());
40 | if(error.hasOwnProperty('response')) {
41 | dispatch(hasError(error.response.data.message));
42 | }
43 | });
44 | }
45 | }
46 |
47 | useEffect(() => {
48 | /* global google */
49 | google.accounts.id.initialize({
50 | client_id: process.env.REACT_APP_SSO_GOOGLE_IDENITY_SERVICES_CLIENT_ID,
51 | callback: googleIdentityServices
52 | });
53 |
54 | if(addButton) {
55 | google.accounts.id.renderButton(
56 | document.getElementById("buttonDiv"),
57 | { theme: "filled_black", size: "large", width: "350px", text: text });
58 | }
59 |
60 | if(addPrompt) {
61 | google.accounts.id.prompt();
62 | }
63 | // eslint-disable-next-line react-hooks/exhaustive-deps
64 | }, [])
65 |
66 | return (
67 | <>
68 | {addButton && }
69 | >
70 | )
71 | }
72 |
73 | export default GoogleIdentityServices;
--------------------------------------------------------------------------------
/frontend/src/pages/Public/AccountActivation/AccountActivation.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useParams, useNavigate } from 'react-router-dom';
3 | import axios from 'axios';
4 | import style from './AccountActivation.module.css';
5 | import logo from '../../../assets/logo-header.png';
6 |
7 | import { useSelector, useDispatch } from 'react-redux';
8 | import { setNotDisable, hasError, hasNoError } from '../../../actions';
9 | import { AllReducers } from '../../../interfaces';
10 | import Layout from '../../../components/AllRoutes/Layout/Layout';
11 |
12 | const AccountActivation = () => {
13 | const navigate = useNavigate();
14 | const { token } = useParams();
15 | const [isActivated, setIsActivated] = useState(false);
16 | const error = useSelector((state: AllReducers) => state.error);
17 | const dispatch = useDispatch();
18 |
19 | useEffect(() => {
20 | if(token) {
21 | axios.post(`${process.env.REACT_APP_API}/api/v1/authentication/activate`, {
22 | token: token
23 | })
24 | .then((response) => {
25 | if(response.status === 200 && response.data.status === 'ok') {
26 | dispatch(setNotDisable());
27 | if(error.hasError) {
28 | dispatch(hasNoError());
29 | }
30 | setIsActivated(true);
31 | navigate('/home');
32 | }
33 | })
34 | .catch(function (error) {
35 | dispatch(setNotDisable());
36 | dispatch(hasError(error.response.data.message));
37 | navigate('/register');
38 | });
39 | }else {
40 | dispatch(setNotDisable());
41 | navigate('/');
42 | }
43 | // eslint-disable-next-line react-hooks/exhaustive-deps
44 | }, []);
45 |
46 | if(!isActivated) {
47 | return (
48 | <>
49 |
50 |
51 |
52 |

53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
Loading...
62 |
Account activation may take a while. Please wait.
63 |
64 |
65 |
66 | >
67 | )
68 | }
69 |
70 | return (
71 | <>
72 | >
73 | )
74 | }
75 |
76 | export default AccountActivation;
--------------------------------------------------------------------------------
/frontend/src/assets/landing_page_icon_4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/Public/FirebaseGoogleSignInButton/FirebaseGoogleSignInButton.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from 'react-router-dom';
2 | import { authentication } from '../../../config/firebase-config';
3 | import { signInWithPopup, GoogleAuthProvider } from "firebase/auth";
4 | import style from './FirebaseGoogleSignInButton.module.css';
5 | import DOMPurify from 'dompurify'; // FOR SANITIZING USER INPUT TO PREVENT XSS ATTACKS BEFORE SENDING TO THE BACKEND
6 | import axios from 'axios';
7 | import google from '../../../assets/google.png';
8 |
9 | import { useSelector, useDispatch } from 'react-redux';
10 | import { setDisable, setNotDisable, hasError, hasNoError } from '../../../actions';
11 | import { AllReducers } from '../../../interfaces';
12 | import { onAuthStateChanged, signOut } from 'firebase/auth';
13 |
14 | type FirebaseGoogleSignInButtonProps = {
15 | text: string
16 | }
17 |
18 | const FirebaseGoogleSignInButton = ({text}: FirebaseGoogleSignInButtonProps) => {
19 | const navigate = useNavigate();
20 | const isDisabled = useSelector((state: AllReducers) => state.isDisabled);
21 | const error = useSelector((state: AllReducers) => state.error);
22 | const dispatch = useDispatch();
23 |
24 | const signInWithGoogle = () => {
25 | const provider = new GoogleAuthProvider();
26 | signInWithPopup(authentication, provider)
27 | .then((response: any) => {
28 | const sanitizedToken = DOMPurify.sanitize(response.user.accessToken);
29 | signOut(authentication);
30 | dispatch(setDisable());
31 | axios.post(`${process.env.REACT_APP_API}/api/v1/authentication/sso/${text === 'signin_with' ? 'sign-in' : 'sign-up'}/firebase-google`, {
32 | token: sanitizedToken
33 | })
34 | .then((response) => {
35 | if(response.status === 200 && response.data.status === 'ok') {
36 | dispatch(setNotDisable());
37 | if(error.hasError) {
38 | dispatch(hasNoError());
39 | }
40 | navigate('/home');
41 | }
42 | })
43 | .catch((error) => {
44 | dispatch(setNotDisable());
45 | if(error.hasOwnProperty('response')) {
46 | dispatch(hasError(error.response.data.message));
47 | }
48 | });
49 | })
50 | .catch((error) => {
51 | dispatch(setNotDisable());
52 | if(error.hasOwnProperty('response')) {
53 | dispatch(hasError(error.response.data.message));
54 | }
55 | });
56 | }
57 |
58 | onAuthStateChanged(authentication, async (user)=>{
59 | if(user) {
60 | signOut(authentication);
61 | }
62 | })
63 |
64 | return (
65 | <>
66 |
70 | >
71 | )
72 | }
73 |
74 | export default FirebaseGoogleSignInButton;
--------------------------------------------------------------------------------
/frontend/src/components/Public/FirebaseFacebookSignInButton/FirebaseFacebookSignInButton.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from 'react-router-dom';
2 | import { authentication } from '../../../config/firebase-config';
3 | import { signInWithPopup, FacebookAuthProvider } from "firebase/auth";
4 | import style from './FirebaseFacebookSignInButton.module.css';
5 | import DOMPurify from 'dompurify'; // FOR SANITIZING USER INPUT TO PREVENT XSS ATTACKS BEFORE SENDING TO THE BACKEND
6 | import axios from 'axios';
7 | import facebook from '../../../assets/facebook.png';
8 |
9 | import { useSelector, useDispatch } from 'react-redux';
10 | import { setDisable, setNotDisable, hasError, hasNoError } from '../../../actions';
11 | import { AllReducers } from '../../../interfaces';
12 | import { onAuthStateChanged, signOut } from 'firebase/auth';
13 |
14 | type FirebaseFacebookSignInButtonProps = {
15 | text: string
16 | }
17 |
18 | const FirebaseFacebookSignInButton = ({text}: FirebaseFacebookSignInButtonProps) => {
19 | const navigate = useNavigate();
20 | const isDisabled = useSelector((state: AllReducers) => state.isDisabled);
21 | const error = useSelector((state: AllReducers) => state.error);
22 | const dispatch = useDispatch();
23 |
24 | const signInWithFacebook = () => {
25 | const provider = new FacebookAuthProvider();
26 | signInWithPopup(authentication, provider)
27 | .then((response: any) => {
28 | const sanitizedToken = DOMPurify.sanitize(response.user.accessToken);
29 | signOut(authentication);
30 | dispatch(setDisable());
31 | axios.post(`${process.env.REACT_APP_API}/api/v1/authentication/sso/${text === 'signin_with' ? 'sign-in' : 'sign-up'}/firebase-facebook`, {
32 | token: sanitizedToken
33 | })
34 | .then((response) => {
35 | if(response.status === 200 && response.data.status === 'ok') {
36 | dispatch(setNotDisable());
37 | if(error.hasError) {
38 | dispatch(hasNoError());
39 | }
40 | navigate('/home');
41 | }
42 | })
43 | .catch((error) => {
44 | dispatch(setNotDisable());
45 | if(error.hasOwnProperty('response')) {
46 | dispatch(hasError(error.response.data.message));
47 | }
48 | });
49 | })
50 | .catch((error) => {
51 | dispatch(setNotDisable());
52 | if(error.hasOwnProperty('response')) {
53 | dispatch(hasError(error.response.data.message));
54 | }
55 | });
56 | }
57 |
58 | onAuthStateChanged(authentication, async (user)=>{
59 | if(user) {
60 | signOut(authentication);
61 | }
62 | })
63 |
64 | return (
65 | <>
66 |
70 | >
71 | )
72 | }
73 |
74 | export default FirebaseFacebookSignInButton;
--------------------------------------------------------------------------------
/frontend/src/pages/Public/ForgotPassword/ForgotPassword.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | width: 100%;
3 | height: 90px;
4 | display: flex;
5 | justify-content: space-between;
6 | padding: 0px 3rem;
7 | align-items: center;
8 | }
9 |
10 | .logo_container {
11 | width: 50px;
12 | display: flex;
13 | justify-content: center;
14 | align-items: center;
15 | }
16 |
17 | .logo {
18 | width: 40px;
19 | object-fit: cover;
20 | }
21 |
22 | .nav_links {
23 | height: 100%;
24 | display: flex;
25 | justify-content: space-around;
26 | align-items: center;
27 | }
28 |
29 | .link {
30 | font-size: .875rem;
31 | line-height: 1.25rem;
32 | padding: 0px 10px;
33 | color: #9ba2ae;
34 | transition: 0.1s;
35 | }
36 |
37 | .link:hover {
38 | color: white;
39 | }
40 |
41 | .forgot_password_form, .recovery_account_link {
42 | width: 350px;
43 | height: 500px;
44 | flex-direction: column;
45 | display: flex;
46 | }
47 |
48 | .main {
49 | margin: 0 auto;
50 | }
51 |
52 | .forgot_password_form_title, .recovery_account_link_title {
53 | text-align: center;
54 | font-family: 'Inter';
55 | font-style: normal;
56 | font-weight: 600;
57 | font-size: 1.5rem;
58 | color: #e1e7ef;
59 | }
60 |
61 | .forgot_password_form_subtitle, .recovery_account_link_subtitle {
62 | text-align: center;
63 | font-family: 'Inter';
64 | font-style: normal;
65 | font-weight: 400;
66 | font-size: .875rem;
67 | line-height: 1.25rem;
68 | color: #7A889D;
69 | margin-top: 15px;
70 | margin-bottom: 20px;
71 | }
72 |
73 | .forgot_password_form_input {
74 | background: #111827;
75 | border-radius: 5px;
76 | margin-top: 10px;
77 | padding-top: 0.5rem;
78 | padding-bottom: 0.5rem;
79 | padding-left: 0.75rem;
80 | padding-right: 0.75rem;
81 | border: 0 solid #e5e7eb;
82 | border-width: 1px;
83 | border-color: hsl(216 34% 17%);
84 | font-size: .875rem;
85 | line-height: 1.25rem;
86 | height: 2.5rem;
87 | width: 100%;
88 | }
89 |
90 | ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
91 | font-family: 'Inter';
92 | font-style: normal;
93 | font-weight: 400;
94 | font-size: .875rem;
95 | line-height: 1.25rem;
96 | color: #7A889D;
97 | }
98 |
99 | .forgot_password_form_input_error {
100 | font-size: .875rem;
101 | line-height: 1.25rem;
102 | margin-top: 5px;
103 | /* color: rgb(206, 48, 0); */
104 | color: rgb(206, 14, 0);
105 | }
106 |
107 | .forgot_password_form_submit {
108 | background: #F8FAFC;
109 | border-radius: 5px;
110 | font-size: .875rem;
111 | line-height: 1.25rem;
112 | padding-left: 1rem;
113 | padding-right: 1rem;
114 | padding-top: 0.5rem;
115 | padding-bottom: 0.5rem;
116 | border: 0 solid #e5e7eb;
117 | color: rgb(2, 2, 5);
118 | font-weight: 500;
119 | margin-top: 10px;
120 | transition: 0.1s;
121 | cursor: pointer;
122 | height: 2.5rem;
123 | width: 100%;
124 | }
125 |
126 | .forgot_password_form_submit[disabled] {
127 | display: flex;
128 | align-items: center;
129 | justify-content: center;
130 | padding-right: 30px;
131 | }
132 |
133 | .forgot_password_form_submit:hover {
134 | opacity: 0.9;
135 | }
136 |
137 | .recovery_account_link {
138 | width: 100%;
139 | display: flex;
140 | align-items: center;
141 | padding: 0rem 3rem;
142 | }
143 |
144 | .recovery_account_link_login {
145 | margin-top: 20px;
146 | display: block;
147 | font-family: 'Inter';
148 | font-style: normal;
149 | font-weight: 400;
150 | font-size: .875rem;
151 | line-height: 1.25rem;
152 | text-decoration-line: underline;
153 | color: #5DB9FC;
154 | text-align: center;
155 | }
156 |
157 |
158 |
159 | @media screen and (max-width: 768px) {
160 | .header {
161 | padding: 0px 1.5rem;
162 | }
163 | }
--------------------------------------------------------------------------------
/frontend/src/pages/Public/Login/Login.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | width: 100%;
3 | height: 90px;
4 | display: flex;
5 | justify-content: space-between;
6 | padding: 0px 3rem;
7 | align-items: center;
8 | }
9 |
10 | .logo_container {
11 | width: 50px;
12 | display: flex;
13 | justify-content: center;
14 | align-items: center;
15 | }
16 |
17 | .logo {
18 | width: 40px;
19 | object-fit: cover;
20 | }
21 |
22 | .nav_links {
23 | height: 100%;
24 | display: flex;
25 | justify-content: space-around;
26 | align-items: center;
27 | }
28 |
29 | .link {
30 | font-size: .875rem;
31 | line-height: 1.25rem;
32 | padding: 0px 10px;
33 | color: #9ba2ae;
34 | transition: 0.1s;
35 | }
36 |
37 | .link:hover {
38 | color: white;
39 | }
40 |
41 | .login_form {
42 | width: 350px;
43 | height: 500px;
44 | flex-direction: column;
45 | display: flex;
46 | }
47 |
48 | .main {
49 | margin: 0 auto;
50 | }
51 |
52 | .login_form_title {
53 | text-align: center;
54 | font-family: 'Inter';
55 | font-style: normal;
56 | font-weight: 600;
57 | font-size: 1.5rem;
58 | color: #e1e7ef;
59 | }
60 |
61 | .login_form_subtitle {
62 | text-align: center;
63 | font-family: 'Inter';
64 | font-style: normal;
65 | font-weight: 400;
66 | font-size: .875rem;
67 | line-height: 1.25rem;
68 | color: #7A889D;
69 | margin-top: 15px;
70 | margin-bottom: 20px;
71 | }
72 |
73 | .login_form_input {
74 | background: #111827;
75 | border-radius: 5px;
76 | margin-top: 10px;
77 | padding-top: 0.5rem;
78 | padding-bottom: 0.5rem;
79 | padding-left: 0.75rem;
80 | padding-right: 0.75rem;
81 | border: 0 solid #e5e7eb;
82 | border-width: 1px;
83 | border-color: hsl(216 34% 17%);
84 | font-size: .875rem;
85 | line-height: 1.25rem;
86 | height: 2.5rem;
87 | width: 100%;
88 | }
89 |
90 | ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
91 | font-family: 'Inter';
92 | font-style: normal;
93 | font-weight: 400;
94 | font-size: .875rem;
95 | line-height: 1.25rem;
96 | color: #7A889D;
97 | }
98 |
99 | .login_form_input_error {
100 | font-size: .875rem;
101 | line-height: 1.25rem;
102 | margin-top: 5px;
103 | /* color: rgb(206, 48, 0); */
104 | color: rgb(206, 14, 0);
105 | }
106 |
107 | .login_form_submit {
108 | background: #F8FAFC;
109 | border-radius: 5px;
110 | font-size: .875rem;
111 | line-height: 1.25rem;
112 | padding-left: 1rem;
113 | padding-right: 1rem;
114 | padding-top: 0.5rem;
115 | padding-bottom: 0.5rem;
116 | border: 0 solid #e5e7eb;
117 | color: rgb(2, 2, 5);
118 | font-weight: 500;
119 | margin-top: 10px;
120 | transition: 0.1s;
121 | cursor: pointer;
122 | height: 2.5rem;
123 | width: 100%;
124 | }
125 |
126 | .login_form_submit[disabled] {
127 | display: flex;
128 | align-items: center;
129 | justify-content: center;
130 | padding-right: 30px;
131 | }
132 |
133 | .login_form_link_forgot_password {
134 | margin-top: 20px;
135 | display: block;
136 | font-family: 'Inter';
137 | font-style: normal;
138 | font-weight: 400;
139 | font-size: .875rem;
140 | line-height: 1.25rem;
141 | text-decoration-line: underline;
142 | color: #5DB9FC;
143 | }
144 |
145 | .login_form_submit:hover {
146 | opacity: 0.9;
147 | }
148 |
149 | .overline_container {
150 | position: relative;
151 | margin-bottom: 10px;
152 | }
153 |
154 | .overline {
155 | width: 100%;
156 | height: 1px;
157 | margin-top: 30px;
158 | background-color: hsl(216 34% 17%);
159 | }
160 |
161 | .overline_text {
162 | position: relative;
163 | text-align: center;
164 | top: -12px;
165 | }
166 |
167 | .overline_text > span {
168 | background-color: #111827;
169 | padding: 0px 10px;
170 | font-size: .75rem;
171 | line-height: 1rem;
172 | color: hsl(215.4 16.3% 56.9%);
173 | }
174 |
175 | @media screen and (max-width: 768px) {
176 | .header {
177 | padding: 0px 1.5rem;
178 | }
179 | }
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import axios from 'axios';
3 | import './App.css';
4 | import style from './App.module.css';
5 | import logo from './assets/logo-header.png';
6 | import icon1 from './assets/landing_page_icon_1.png';
7 | import icon2 from './assets/landing_page_icon_2.svg';
8 | import icon3 from './assets/landing_page_icon_3.svg';
9 | import icon4 from './assets/landing_page_icon_4.svg';
10 | import facebook from './assets/facebook.png';
11 | import linkedin from './assets/linkedin.png';
12 | import github from './assets/github.png';
13 |
14 | axios.defaults.withCredentials = true;
15 |
16 | function App() {
17 | return (
18 | <>
19 |
20 |
31 |
32 |
33 |
34 |
Open Source Project
35 |
MERN with Authentication Boilerplate
36 |
Effortless User Authentication for Your MERN Stack Projects
37 |
38 |
39 |
40 |
41 |
42 |

43 |
44 |
Implemented Strong Security Measures
45 |
46 |
47 |
48 |
49 |

50 |
51 |
Forgot Password, SSO, & MFA Included
52 |
53 |
54 |
55 |
56 |

57 |
58 |
Customizable e-mail templates, cookies, & others.
59 |
60 |
61 |
62 |
63 |

64 |
65 |
and more...
66 |
67 |
68 |
69 |
70 |
71 |
72 |
88 |
89 |
90 | >
91 | )
92 | }
93 |
94 | export default App
95 |
--------------------------------------------------------------------------------
/frontend/src/pages/Public/ResetPassword/ResetPassword.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | width: 100%;
3 | height: 90px;
4 | display: flex;
5 | justify-content: space-between;
6 | padding: 0px 3rem;
7 | align-items: center;
8 | }
9 |
10 | .logo_container {
11 | font-size: .875rem;
12 | line-height: 1.25rem;
13 | padding: 0px 10px;
14 | color: #9ba2ae;
15 | transition: 0.1s;
16 | width: 50px;
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | }
21 |
22 | .logo {
23 | width: 40px;
24 | object-fit: cover;
25 | }
26 |
27 | .nav_links {
28 | height: 100%;
29 | display: flex;
30 | justify-content: space-around;
31 | align-items: center;
32 | }
33 |
34 | .link {
35 | font-size: .875rem;
36 | line-height: 1.25rem;
37 | padding: 0px 10px;
38 | color: #9ba2ae;
39 | transition: 0.1s;
40 | }
41 |
42 | .link:hover {
43 | color: white;
44 | }
45 |
46 | .reset_password_form, .activation_link_container {
47 | width: 350px;
48 | height: 500px;
49 | flex-direction: column;
50 | display: flex;
51 | }
52 |
53 | .main {
54 | margin: 0 auto;
55 | }
56 |
57 | .reset_password_form_title, .activation_link_container_title {
58 | text-align: center;
59 | font-family: 'Inter';
60 | font-style: normal;
61 | font-weight: 600;
62 | font-size: 1.5rem;
63 | color: #e1e7ef;
64 | }
65 |
66 | .reset_password_form_subtitle, .activation_link_container_subtitle {
67 | text-align: center;
68 | font-family: 'Inter';
69 | font-style: normal;
70 | font-weight: 400;
71 | font-size: .875rem;
72 | line-height: 1.25rem;
73 | color: #7A889D;
74 | margin-top: 15px;
75 | margin-bottom: 20px;
76 | }
77 |
78 | .reset_password_form_input {
79 | background: #111827;
80 | border-radius: 5px;
81 | margin-top: 10px;
82 | padding-top: 0.5rem;
83 | padding-bottom: 0.5rem;
84 | padding-left: 0.75rem;
85 | padding-right: 0.75rem;
86 | border: 0 solid #e5e7eb;
87 | border-width: 1px;
88 | border-color: hsl(216 34% 17%);
89 | font-size: .875rem;
90 | line-height: 1.25rem;
91 | height: 2.5rem;
92 | width: 100%;
93 | }
94 |
95 | ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
96 | font-family: 'Inter';
97 | font-style: normal;
98 | font-weight: 400;
99 | font-size: .875rem;
100 | line-height: 1.25rem;
101 | color: #7A889D;
102 | }
103 |
104 | .reset_password_form_input_error {
105 | font-size: .875rem;
106 | line-height: 1.25rem;
107 | margin-top: 5px;
108 | color: rgb(206, 14, 0);
109 | }
110 |
111 | .reset_password_form_submit {
112 | background: #F8FAFC;
113 | border-radius: 5px;
114 | font-size: .875rem;
115 | line-height: 1.25rem;
116 | padding-left: 1rem;
117 | padding-right: 1rem;
118 | padding-top: 0.5rem;
119 | padding-bottom: 0.5rem;
120 | border: 0 solid #e5e7eb;
121 | color: rgb(2, 2, 5);
122 | font-weight: 500;
123 | margin-top: 10px;
124 | transition: 0.1s;
125 | cursor: pointer;
126 | height: 2.5rem;
127 | width: 100%;
128 | }
129 |
130 | .reset_password_form_submit[disabled] {
131 | display: flex;
132 | align-items: center;
133 | justify-content: center;
134 | padding-right: 30px;
135 | }
136 |
137 | .reset_password_form_link_login, .activation_link_login {
138 | margin-top: 20px;
139 | display: block;
140 | font-family: 'Inter';
141 | font-style: normal;
142 | font-weight: 400;
143 | font-size: .875rem;
144 | line-height: 1.25rem;
145 | text-decoration-line: underline;
146 | color: #5DB9FC;
147 | }
148 |
149 | .reset_password_form_submit:hover {
150 | opacity: 0.9;
151 | }
152 |
153 | .reset_password_verify_token {
154 | width: 350px;
155 | height: 500px;
156 | flex-direction: column;
157 | display: flex;
158 | margin-top: -100px;
159 | }
160 |
161 | .main {
162 | display: flex;
163 | justify-content: center;
164 | align-items: center;
165 | }
166 |
167 | .reset_password_verify_token_title {
168 | text-align: center;
169 | font-family: 'Inter';
170 | font-style: normal;
171 | font-weight: 600;
172 | font-size: 1.5rem;
173 | color: #e1e7ef;
174 | }
175 |
176 | .reset_password_verify_token_subtitle {
177 | text-align: center;
178 | font-family: 'Inter';
179 | font-style: normal;
180 | font-weight: 400;
181 | font-size: .875rem;
182 | line-height: 1.25rem;
183 | color: #7A889D;
184 | margin-top: 15px;
185 | margin-bottom: 20px;
186 | }
187 |
188 | @media screen and (max-width: 768px) {
189 | .header {
190 | padding: 0px 1.5rem;
191 | }
192 | }
--------------------------------------------------------------------------------
/frontend/src/App.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | width: 100vw;
3 | min-height: 100vh;
4 | height: auto;
5 | background-image: url('./assets/login-background.svg');
6 | display: grid;
7 | grid-template-rows: auto 1fr auto;
8 | grid-template-columns: 100%;
9 | }
10 |
11 | .header {
12 | width: 100%;
13 | height: 90px;
14 | display: flex;
15 | justify-content: space-between;
16 | padding: 0px 3rem;
17 | align-items: center;
18 | }
19 |
20 | .logo_container {
21 | width: 50px;
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | }
26 |
27 | .logo {
28 | width: 40px;
29 | object-fit: cover;
30 | }
31 |
32 | .nav_links {
33 | height: 100%;
34 | display: flex;
35 | justify-content: space-around;
36 | align-items: center;
37 | }
38 |
39 | .link {
40 | font-size: .875rem;
41 | line-height: 1.25rem;
42 | padding: 0px 10px;
43 | color: #9ba2ae;
44 | transition: 0.1s;
45 | }
46 |
47 | .link:hover {
48 | color: white;
49 | }
50 |
51 | .main {
52 | display: flex;
53 | justify-content: center;
54 | align-items: center;
55 | }
56 |
57 | .hero_container {
58 | max-width: 750px;
59 | height: auto;
60 | display: flex;
61 | align-items: center;
62 | flex-direction: column;
63 | text-align: center;
64 | margin-top: -130px;
65 | }
66 |
67 | .hero_tag {
68 | width: auto;
69 | padding: 0px 30px;
70 | height: 40px;
71 | background: rgba(74, 74, 74, 0.21);
72 | display: flex;
73 | justify-content: center;
74 | align-items: center;
75 | border-radius: 20px;
76 | font-family: 'Inter';
77 | font-style: normal;
78 | font-weight: 400;
79 | font-size: 16px;
80 | line-height: 20px;
81 | }
82 |
83 | .hero_title {
84 | width: 100%;
85 | margin-top: 35px;
86 | font-family: 'Inter';
87 | font-style: normal;
88 | font-weight: 700;
89 | font-size: 2.8rem;
90 | line-height: 49px;
91 | text-align: center;
92 | }
93 |
94 | .hero_tagline {
95 | width: 80%;
96 | margin-top: 40px;
97 | font-size: 1.3rem;
98 | color: white;
99 | font-family: 'Inter';
100 | font-style: normal;
101 | font-weight: 400;
102 | line-height: 24px;
103 | text-align: center;
104 | }
105 |
106 | .hero_features_container {
107 | margin-top: 60px;
108 | height: 100%;
109 | width: 100%;
110 | display: flex;
111 | gap: 10px;
112 | justify-content: center;
113 | flex-wrap: wrap;
114 | }
115 |
116 | .hero_feature {
117 | width: 180px;
118 | background-color: #1B2844;
119 | border-radius: 5px;
120 | display: flex;
121 | flex-direction: column;
122 | padding: 30px 15px;
123 | font-family: 'Inter';
124 | font-style: normal;
125 | font-weight: 400;
126 | text-align: center;
127 | }
128 |
129 | .hero_feature_icon_container {
130 | width: 100%;
131 | height: 50px;
132 | display: flex;
133 | justify-content: center;
134 | align-items: center;
135 | }
136 |
137 | .hero_feature_icon {
138 | width: 45px;
139 | }
140 |
141 | .hero_feature_title {
142 | font-size: 1rem;
143 | margin-top: 20px;
144 | }
145 |
146 | .footer_info_container {
147 | width: 100%;
148 | height: 90px;
149 | display: flex;
150 | justify-content: space-between;
151 | align-items: center;
152 | padding: 0px 3rem;
153 | }
154 |
155 | .footer_info_developer {
156 | width: auto;
157 | font-family: 'Inter';
158 | font-style: normal;
159 | font-weight: 400;
160 | font-size: 1rem;
161 | line-height: 20px;
162 | color: #545E80;
163 | }
164 |
165 | .footer_info_developer_social {
166 | height: 100%;
167 | display: flex;
168 | justify-content: space-around;
169 | align-items: center;
170 | gap: 15px;
171 | }
172 |
173 | .footer_info_deveveloper_social_icon {
174 | width: 28px;
175 | }
176 |
177 | @media screen and (max-width: 768px) {
178 | .header {
179 | padding: 0px 1.5rem;
180 | }
181 |
182 | .hero_container {
183 | margin-top: 0px;
184 | padding: 15px;
185 | }
186 |
187 | .hero_tag {
188 | display: none;
189 | }
190 |
191 | .hero_title {
192 | font-size: 2.2rem;
193 | }
194 |
195 | .hero_tagline {
196 | font-size: 1rem;
197 | }
198 |
199 | .hero_feature {
200 | width: 150px;
201 | }
202 |
203 | .footer_info_container {
204 | margin-top: 30px;
205 | flex-direction: column;
206 | }
207 |
208 | .footer_info_developer {
209 | font-size: 0.9rem;
210 | }
211 |
212 | .footer_info_deveveloper_social_icon {
213 | width: 24px;
214 | }
215 | }
--------------------------------------------------------------------------------
/frontend/src/pages/Public/Register/Register.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | width: 100%;
3 | height: 90px;
4 | display: flex;
5 | justify-content: space-between;
6 | padding: 0px 3rem;
7 | align-items: center;
8 | }
9 |
10 | .logo_container {
11 | width: 50px;
12 | display: flex;
13 | justify-content: center;
14 | align-items: center;
15 | }
16 |
17 | .logo {
18 | width: 40px;
19 | object-fit: cover;
20 | }
21 |
22 | .nav_links {
23 | height: 100%;
24 | display: flex;
25 | justify-content: space-around;
26 | align-items: center;
27 | }
28 |
29 | .link {
30 | font-size: .875rem;
31 | line-height: 1.25rem;
32 | padding: 0px 10px;
33 | color: #9ba2ae;
34 | transition: 0.1s;
35 | }
36 |
37 | .link:hover {
38 | color: white;
39 | }
40 |
41 | .register_form, .activation_link_container {
42 | width: 350px;
43 | height: 500px;
44 | flex-direction: column;
45 | display: flex;
46 | }
47 |
48 | .main {
49 | margin: 0 auto;
50 | }
51 |
52 | .register_form_title, .activation_link_container_title {
53 | text-align: center;
54 | font-family: 'Inter';
55 | font-style: normal;
56 | font-weight: 600;
57 | font-size: 1.5rem;
58 | color: #e1e7ef;
59 | }
60 |
61 | .register_form_subtitle, .activation_link_container_subtitle {
62 | text-align: center;
63 | font-family: 'Inter';
64 | font-style: normal;
65 | font-weight: 400;
66 | font-size: .875rem;
67 | line-height: 1.25rem;
68 | color: #7A889D;
69 | margin-top: 15px;
70 | margin-bottom: 20px;
71 | }
72 |
73 | .register_form_input {
74 | background: #111827;
75 | border-radius: 5px;
76 | margin-top: 10px;
77 | padding-top: 0.5rem;
78 | padding-bottom: 0.5rem;
79 | padding-left: 0.75rem;
80 | padding-right: 0.75rem;
81 | border: 0 solid #e5e7eb;
82 | border-width: 1px;
83 | border-color: hsl(216 34% 17%);
84 | font-size: .875rem;
85 | line-height: 1.25rem;
86 | height: 2.5rem;
87 | width: 100%;
88 | }
89 |
90 | ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
91 | font-family: 'Inter';
92 | font-style: normal;
93 | font-weight: 400;
94 | font-size: .875rem;
95 | line-height: 1.25rem;
96 | color: #7A889D;
97 | }
98 |
99 | .register_form_input_error {
100 | font-size: .875rem;
101 | line-height: 1.25rem;
102 | margin-top: 5px;
103 | /* color: rgb(206, 48, 0); */
104 | color: rgb(206, 14, 0);
105 | }
106 |
107 | .register_form_submit {
108 | background: #F8FAFC;
109 | border-radius: 5px;
110 | font-size: .875rem;
111 | line-height: 1.25rem;
112 | padding-left: 1rem;
113 | padding-right: 1rem;
114 | padding-top: 0.5rem;
115 | padding-bottom: 0.5rem;
116 | border: 0 solid #e5e7eb;
117 | color: rgb(2, 2, 5);
118 | font-weight: 500;
119 | margin-top: 10px;
120 | transition: 0.1s;
121 | cursor: pointer;
122 | height: 2.5rem;
123 | width: 100%;
124 | }
125 |
126 | .register_form_submit[disabled] {
127 | display: flex;
128 | align-items: center;
129 | justify-content: center;
130 | padding-right: 30px;
131 | }
132 |
133 | .register_form_link_login, .activation_link_login {
134 | margin-top: 20px;
135 | display: block;
136 | font-family: 'Inter';
137 | font-style: normal;
138 | font-weight: 400;
139 | font-size: .875rem;
140 | line-height: 1.25rem;
141 | text-decoration-line: underline;
142 | color: #5DB9FC;
143 | text-align: center;
144 | }
145 |
146 | .activation_link_login {
147 | text-align: center;
148 | }
149 |
150 | .register_form_submit:hover {
151 | opacity: 0.9;
152 | }
153 |
154 | .overline_container {
155 | position: relative;
156 | margin-bottom: 10px;
157 | }
158 |
159 | .overline {
160 | width: 100%;
161 | height: 1px;
162 | margin-top: 30px;
163 | background-color: hsl(216 34% 17%);
164 | }
165 |
166 | .overline_text {
167 | position: relative;
168 | text-align: center;
169 | top: -12px;
170 | }
171 |
172 | .overline_text > span {
173 | background-color: #111827;
174 | padding: 0px 10px;
175 | font-size: .75rem;
176 | line-height: 1rem;
177 | color: hsl(215.4 16.3% 56.9%);
178 | }
179 |
180 | .register_form_term_privacy_policy {
181 | padding-left: 2rem;
182 | padding-right: 2rem;
183 | text-align: center;
184 | font-size: .875rem;
185 | line-height: 1.25rem;
186 | margin-top: calc(1.5rem * calc(1 - 0));
187 | color: #7A889D;
188 | }
189 |
190 | .register_form_link_term_policy, .register_form_link_privacy_policy {
191 | text-decoration: underline;
192 | }
193 |
194 | .register_form_link_term_policy:hover, .register_form_link_privacy_policy:hover {
195 | color: white;
196 | }
197 |
198 | @media screen and (max-width: 768px) {
199 | .header {
200 | padding: 0px 1.5rem;
201 | }
202 | }
--------------------------------------------------------------------------------
/backend/src/routes/v1AuthenticationRouter.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 |
3 | // * ------------ CONTROLLERS --------------------
4 | import v1AuthenticationController from '../controllers/v1AuthenticationController.js';
5 | // * ------------ CONTROLLERS --------------------
6 |
7 | // * ------------ middleware --------------------
8 | import * as middlewareLimiter from '../middlewares/v1AuthenticationLimiter.js';
9 | import * as middleware from '../middlewares/index.js';
10 | // * ------------ middleware --------------------
11 |
12 | const router = express.Router();
13 |
14 | // * API THAT VERIFY PUBLIC CSRF TOKEN IN THE MIDDLEWARE
15 | router.post('/register',
16 | middlewareLimiter.registerLimiter,
17 | middleware.verifyPublicCSRFToken,
18 | v1AuthenticationController.register);
19 |
20 | router.post('/login',
21 | middlewareLimiter.loginLimiter,
22 | middleware.verifyPublicCSRFToken,
23 | v1AuthenticationController.login);
24 |
25 | router.post('/activate',
26 | middlewareLimiter.activateLimiter,
27 | middleware.verifyPublicCSRFToken,
28 | v1AuthenticationController.activate);
29 |
30 | router.post('/forgot-password',
31 | middlewareLimiter.forgotPasswordLimiter,
32 | middleware.verifyPublicCSRFToken,
33 | v1AuthenticationController.forgotPassword);
34 |
35 | // * API TWO/MULTI FACTOR AUTHENTICATION
36 | router.post('/verification-code-login',
37 | middlewareLimiter.verificationCodeLoginLimiter,
38 | middleware.verifyPublicCSRFToken,
39 | v1AuthenticationController.verificationCodeLogin);
40 |
41 | router.post('/verification-code-login/logout',
42 | middlewareLimiter.verificationCodeLoginLogoutLimiter,
43 | middleware.verifyPublicCSRFToken,
44 | v1AuthenticationController.verificationCodeLoginLogout);
45 |
46 | router.post('/google-authenticator-code-login',
47 | middlewareLimiter.verificationCodeLoginLimiter,
48 | middleware.verifyPublicCSRFToken,
49 | v1AuthenticationController.googleAuthenticatorCodeLogin);
50 |
51 | // * API SINGLE SIGN ON
52 | router.post('/sso/sign-in/google-identity-services',
53 | middlewareLimiter.loginLimiter,
54 | middleware.verifyPublicCSRFToken,
55 | v1AuthenticationController.ssoSignInGoogleIdentityServices);
56 |
57 | router.post('/sso/sign-up/google-identity-services',
58 | middlewareLimiter.loginLimiter,
59 | middleware.verifyPublicCSRFToken,
60 | v1AuthenticationController.ssoSignUpGoogleIdentityServices);
61 |
62 | router.post('/sso/sign-in/firebase-facebook',
63 | middlewareLimiter.loginLimiter,
64 | middleware.verifyPublicCSRFToken,
65 | v1AuthenticationController.ssoSignInFirebaseFacebook);
66 |
67 | router.post('/sso/sign-up/firebase-facebook',
68 | middlewareLimiter.loginLimiter,
69 | middleware.verifyPublicCSRFToken,
70 | v1AuthenticationController.ssoSignInFirebaseFacebook);
71 |
72 | router.post('/sso/sign-in/firebase-google',
73 | middlewareLimiter.loginLimiter,
74 | middleware.verifyPublicCSRFToken,
75 | v1AuthenticationController.ssoSignInFirebaseGoogle);
76 |
77 | router.post('/sso/sign-up/firebase-google',
78 | middlewareLimiter.loginLimiter,
79 | middleware.verifyPublicCSRFToken,
80 | v1AuthenticationController.ssoSignUpFirebaseGoogle);
81 |
82 | // * API THAT VERIFY PRIVATE CSRF TOKEN FIRST IN THE MIDDLEWARE
83 | router.get('/user',
84 | middlewareLimiter.userLimiter,
85 | middleware.isMFAMode,
86 | middleware.sendPublicCSRFTokenToUser,
87 | middleware.isAuthenticated,
88 | v1AuthenticationController.user);
89 |
90 | // * USER MUST BE AUTHETICATED
91 | router.delete('/user',
92 | middlewareLimiter.deleteUserLimiter,
93 | middleware.sendPublicCSRFTokenToUser,
94 | middleware.isAuthenticated,
95 | v1AuthenticationController.deleteUser);
96 |
97 | router.post('/logout',
98 | middlewareLimiter.logoutLimiter,
99 | middleware.sendPublicCSRFTokenToUser,
100 | middleware.isAuthenticated,
101 | v1AuthenticationController.logout);
102 |
103 | router.post('/user/enable-google-authenticator',
104 | middlewareLimiter.enableGoogleAuthenticatorLimiter,
105 | middleware.isAuthenticated,
106 | v1AuthenticationController.enableGoogleAuthenticator);
107 |
108 | router.post('/user/activate-google-authenticator',
109 | middlewareLimiter.activateGoogleAuthenticatorLimiter,
110 | middleware.isAuthenticated,
111 | v1AuthenticationController.activateGoogleAuthenticator);
112 |
113 | router.post('/user/disable-google-authenticator',
114 | middlewareLimiter.disableGoogleAuthenticatorLimiter,
115 | middleware.isAuthenticated,
116 | v1AuthenticationController.disableGoogleAuthenticator);
117 |
118 | // * API THAT VERIFY PRIVATE CSRF TOKEN VIA REQUEST BODY INSIDE CONTROLLER
119 | router.post('/reset-password',
120 | middlewareLimiter.resetPasswordLimiter,
121 | v1AuthenticationController.resetPassword);
122 |
123 | router.post('/account-recovery/reset-password/verify-token',
124 | middlewareLimiter.resetPasswordVerifyTokenLimiter,
125 | v1AuthenticationController.accountRecoveryResetPasswordVerifyToken);
126 |
127 | export default router;
--------------------------------------------------------------------------------
/backend/src/models/userModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema } from 'mongoose';
2 | import he from 'he';
3 | import argon2 from 'argon2';
4 | import { User } from '../interfaces/index.js';
5 |
6 | const userSchema: Schema = new Schema(
7 | {
8 | username: {
9 | type: String,
10 | unique: true,
11 | trim: true,
12 | required: [true, 'Username is required'],
13 | minlength: [4, 'Username must be at least 4 characters'],
14 | maxlength: [20, 'Username must not exceed 20 characters'],
15 | match: [/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'],
16 | validate: [
17 | {
18 | validator: function(value: string) {
19 | return !/\b(admin|root|superuser)\b/i.test(value);
20 | },
21 | message: 'Username should not contain sensitive information',
22 | },
23 | {
24 | validator: function(value: string) {
25 | const sanitizedValue = he.escape(value);
26 | return sanitizedValue === value;
27 | },
28 | message: 'Invalid characters detected',
29 | },
30 | ],
31 | },
32 | email: {
33 | type: String,
34 | unique: true,
35 | trim: true,
36 | required: [true, 'Email is required'],
37 | match: [
38 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
39 | 'Please enter a valid email address',
40 | ],
41 | validate: [
42 | {
43 | validator: function(value: string) {
44 | const sanitizedValue = he.escape(value);
45 | return sanitizedValue === value;
46 | },
47 | message: 'Invalid email format or potentially unsafe characters',
48 | },
49 | ],
50 | },
51 | password: {
52 | type: String,
53 | select: false,
54 | required: [true, 'Password is required'],
55 | minlength: [12, 'Password must be at least 12 characters'],
56 | match: [
57 | /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()\-_=+{};:,<.>]).+$/,
58 | 'Password must contain at least one uppercase letter, one lowercase letter, one digit, and one special character',
59 | ],
60 | validate: [
61 | {
62 | validator: function(value: string) {
63 | return !/\b(password|123456789)\b/i.test(value);
64 | },
65 | message: 'Password should not be commonly used or easily guessable',
66 | },
67 | ]
68 | },
69 | forgotPassword: {
70 | type: 'boolean',
71 | select: false,
72 | required: false,
73 | default: false
74 | },
75 | isSSO: {
76 | type: 'boolean',
77 | required: true,
78 | default: false
79 | },
80 | verificationCodeLogin: {
81 | type: String,
82 | select: false,
83 | minlength: [7, 'Verification login code must be 7 characters'],
84 | maxlength: [7, 'Verification login code must be 7 characters'],
85 | match: [
86 | /^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]{7}$/,
87 | 'Verification login code must be 7 characters and contain only numbers and letters',
88 | ],
89 | validate: [
90 | {
91 | validator: function(value: string) {
92 | return !/\b(admin|root|superuser)\b/i.test(value);
93 | },
94 | message: 'Verification login code should not contain sensitive information',
95 | },{
96 | validator: function(value: string) {
97 | const sanitizedValue = he.escape(value);
98 | return sanitizedValue === value;
99 | },
100 | message: 'Invalid verification login code format or potentially unsafe characters',
101 | },
102 | ]
103 | },
104 | googleAuthenticator: {
105 | type: Schema.Types.ObjectId,
106 | select: false,
107 | ref: 'GoogleAuthentication',
108 | required: false
109 | },
110 | csrfTokenSecret: {
111 | type: Schema.Types.ObjectId,
112 | select: false,
113 | ref: 'CSRFTokenSecret',
114 | required: true
115 | },
116 | profile: {
117 | type: Schema.Types.ObjectId,
118 | ref: 'Profile',
119 | required: true
120 | },
121 | social_id: {
122 | type: String,
123 | required: false
124 | }
125 | },
126 | { timestamps: true, versionKey: false }
127 | );
128 |
129 | userSchema.pre('save', async function (this: User, next: any) {
130 | if (!this.isModified('password')) {
131 | return next();
132 | }
133 |
134 | try {
135 | const hashedPassword = await argon2.hash(this.password);
136 | this.password = hashedPassword;
137 | return next();
138 | } catch (error) {
139 | return next(error);
140 | }
141 | });
142 |
143 | userSchema.methods.matchPasswords = async function (password: string) {
144 | return await argon2.verify(this.password, password);
145 | };
146 |
147 | userSchema.methods.matchVerificationCodeLogin = async function (verificationCodeLogin: string) {
148 | return await argon2.verify(this.verificationCodeLogin, verificationCodeLogin);
149 | };
150 |
151 | export default mongoose.model('User', userSchema);
--------------------------------------------------------------------------------
/frontend/src/pages/MFA/LoginVerificationCode/LoginVerificationCode.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | width: 100%;
3 | height: 90px;
4 | display: flex;
5 | justify-content: space-between;
6 | padding: 0px 3rem;
7 | align-items: center;
8 | }
9 |
10 | .logo_container {
11 | width: 50px;
12 | display: flex;
13 | justify-content: center;
14 | align-items: center;
15 | }
16 |
17 | .logo {
18 | width: 30px;
19 | object-fit: cover;
20 | }
21 |
22 | .nav_links {
23 | height: 100%;
24 | display: flex;
25 | justify-content: space-around;
26 | align-items: center;
27 | font-size: 0.875rem;
28 | }
29 |
30 | .link {
31 | font-size: .875rem;
32 | padding: 0px 10px;
33 | color: #9ba2ae;
34 | transition: 0.1s;
35 | cursor: pointer;
36 | }
37 |
38 | .link:hover {
39 | color: white;
40 | }
41 |
42 | .login_verification_code_form, .activation_link_container {
43 | width: 350px;
44 | height: 500px;
45 | flex-direction: column;
46 | display: flex;
47 | }
48 |
49 | .main {
50 | margin: 0 auto;
51 | }
52 |
53 | .login_verification_code_form_title, .activation_link_container_title {
54 | text-align: center;
55 | font-family: 'Inter';
56 | font-style: normal;
57 | font-weight: 600;
58 | font-size: 1.5rem;
59 | color: #e1e7ef;
60 | }
61 |
62 | .login_verification_code_form_subtitle, .activation_link_container_subtitle {
63 | text-align: center;
64 | font-family: 'Inter';
65 | font-style: normal;
66 | font-weight: 400;
67 | font-size: .875rem;
68 | line-height: 1.25rem;
69 | color: #7A889D;
70 | margin-top: 15px;
71 | margin-bottom: 20px;
72 | }
73 |
74 | .login_verification_code_form_input {
75 | background: #111827;
76 | border-radius: 5px;
77 | margin-top: 10px;
78 | padding-top: 0.5rem;
79 | padding-bottom: 0.5rem;
80 | padding-left: 0.75rem;
81 | padding-right: 0.75rem;
82 | border: 0 solid #e5e7eb;
83 | border-width: 1px;
84 | border-color: hsl(216 34% 17%);
85 | font-size: .875rem;
86 | line-height: 1.25rem;
87 | height: 2.5rem;
88 | width: 100%;
89 | text-align: center;
90 | }
91 |
92 | ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
93 | font-family: 'Inter';
94 | font-style: normal;
95 | font-weight: 400;
96 | font-size: .875rem;
97 | line-height: 1.25rem;
98 | color: #7A889D;
99 | }
100 |
101 | .login_verification_code_form_input_error {
102 | font-size: .875rem;
103 | line-height: 1.25rem;
104 | margin-top: 5px;
105 | /* color: rgb(206, 48, 0); */
106 | color: rgb(206, 14, 0);
107 | }
108 |
109 | .login_verification_code_form_submit {
110 | background: #F8FAFC;
111 | border-radius: 5px;
112 | font-size: .875rem;
113 | line-height: 1.25rem;
114 | padding-left: 1rem;
115 | padding-right: 1rem;
116 | padding-top: 0.5rem;
117 | padding-bottom: 0.5rem;
118 | border: 0 solid #e5e7eb;
119 | color: rgb(2, 2, 5);
120 | font-weight: 500;
121 | margin-top: 10px;
122 | transition: 0.1s;
123 | cursor: pointer;
124 | height: 2.5rem;
125 | width: 100%;
126 | }
127 |
128 | .login_verification_code_form_submit[disabled] {
129 | display: flex;
130 | align-items: center;
131 | justify-content: center;
132 | padding-right: 30px;
133 | }
134 |
135 | .login_verification_code_form_link_login, .activation_link_login {
136 | margin-top: 20px;
137 | display: block;
138 | font-family: 'Inter';
139 | font-style: normal;
140 | font-weight: 400;
141 | font-size: .875rem;
142 | line-height: 1.25rem;
143 | text-decoration-line: underline;
144 | color: #5DB9FC;
145 | }
146 |
147 | .activation_link_login {
148 | text-align: center;
149 | }
150 |
151 | .login_verification_code_form_submit:hover {
152 | opacity: 0.9;
153 | }
154 |
155 | .overline_container {
156 | position: relative;
157 | margin-bottom: 10px;
158 | }
159 |
160 | .overline {
161 | width: 100%;
162 | height: 1px;
163 | margin-top: 30px;
164 | background-color: hsl(216 34% 17%);
165 | }
166 |
167 | .overline_text {
168 | position: relative;
169 | text-align: center;
170 | top: -12px;
171 | }
172 |
173 | .overline_text > span {
174 | background-color: #111827;
175 | padding: 0px 10px;
176 | font-size: .75rem;
177 | line-height: 1rem;
178 | color: hsl(215.4 16.3% 56.9%);
179 | }
180 |
181 | .button_dark {
182 | background: #111827;
183 | border-radius: 5px;
184 | margin-top: 15px;
185 | padding-top: 0.5rem;
186 | padding-bottom: 0.5rem;
187 | padding-left: 0.75rem;
188 | padding-right: 0.75rem;
189 | border: 0 solid #e5e7eb;
190 | border-width: 1px;
191 | border-color: hsl(216 34% 17%);
192 | font-size: .875rem;
193 | line-height: 1.25rem;
194 | height: 2.5rem;
195 | display: flex;
196 | justify-content: center;
197 | align-items: center;
198 | gap: 8px;
199 | transition: 0.1s;
200 | font-weight: 500;
201 | width: 100%;
202 |
203 | }
204 |
205 | .button_dark:hover {
206 | cursor: pointer;
207 | background-color: #1D283A;
208 | }
209 |
210 | .profile_picture {
211 | border-radius: 3px;
212 | }
213 |
214 | @media screen and (max-width: 768px) {
215 | .header {
216 | padding: 0px 1.5rem;
217 | }
218 |
219 | .hero_container {
220 | margin-top: 0px;
221 | padding: 15px;
222 | }
223 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Typescript MERN with Authentication using JWT Boilerplate
2 |
3 | 
4 |
5 | This is my boilerplate for building MERN Stack application monolithic architecture. Most application need authencation and building MERN stack starting with authentication is very hassle that's why I decided to create a ready made MERN stack with authentication.
6 |
7 | Feel free to fork this project to improve the UI, structure, performance, and security of this application.
8 |
9 | ## How to use?
10 |
11 | #### 1. Clone the repo.
12 | ```sh
13 | $ git clone https://github.com/GabrielSalangsang013/ts-mern-with-auth-boilerplate.git
14 | ```
15 |
16 | #### 2. Open the project in VS code.
17 |
18 | #### 3. Open new VS code terminal.
19 |
20 | #### 4. Go inside the backend folder, and install all the packages.
21 | ```sh
22 | $ cd backend
23 | $ npm install
24 | ```
25 |
26 | #### 5. Create .env file and fill up the credentials (still inside backend folder).
27 | ```
28 | # TYPE OF NODE ENVIRONMENT
29 | NODE_ENV=DEVELOPMENT
30 |
31 | # PORT OF SERVER
32 | PORT=4000
33 |
34 | # VERSION OF NODE
35 | NODE_VERSION=18.16.0
36 |
37 | # REACT URL - FRONTEND
38 | REACT_URL=http://localhost:3000
39 |
40 | # MONGO DATABASE URI
41 | MONGO_DB_URI=
42 | MONGO_DB_URI_LIMITER=
43 |
44 | # SECRETS
45 | # (Create secret using this code "node require('crypto').randomBytes(64).toString('hex');")
46 | AUTHENTICATION_TOKEN_SECRET=
47 | ACCOUNT_ACTIVATION_TOKEN_SECRET=
48 | ACCOUNT_RECOVERY_RESET_PASSWORD_TOKEN_SECRET=
49 | ACCOUNT_RECOVERY_RESET_PASSWORD_CSRF_TOKEN_SECRET=
50 | MFA_TOKEN_SECRET=
51 | PUBLIC_CSRF_TOKEN_SECRET=
52 |
53 | # SMTP CONFIGURATION. USED BREVO FORMERLY SENDINBLUE EMAIL SERVICES
54 | SMTP_HOST=
55 | SMTP_PORT=
56 | SMTP_USER=
57 | SMTP_PASSWORD=
58 | EMAIL_FROM=
59 |
60 | # GOOGLE AUTHENTICATOR SECRET
61 | GOOGLE_AUTHENTICATOR_NAME=MERN-Auth
62 |
63 | # SSO GOOGLE IDENTITY SERVICES
64 | # (Follow this tutorial on how to create GIS Client ID - [https://youtu.be/HtJKUQXmtok?si=gwlFBdj-vIt-XoSR])
65 | GOOGLE_IDENITY_SERVICES_CLIENT_ID=
66 | ```
67 |
68 | #### 6. Go to [https://firebase.google.com/](firebase) and create a firebase project.
69 |
70 | #### 7. In your firebase project, go to the project settings.
71 | 
72 |
73 | #### 8. In project settings, go to the "Service accounts" tab, click "Generate new private key" button, and click "Generate key".
74 | 
75 |
76 | #### 9. After that you will receive a downloaded file which contains the firebase admin key. Open the file and copy all the contents.
77 |
78 | #### 10. Now go back in the VS code and go inside the backend project, go to the src/config folder and create firebase-credential.json and paste the Firebase admin key.
79 | ```
80 | {
81 | "type": ,
82 | "project_id": ,
83 | "private_key_id": ,
84 | "private_key": ,
85 | "client_email": ,
86 | "client_id": ,
87 | "auth_uri": ,
88 | "token_uri": ,
89 | "auth_provider_x509_cert_url": ,
90 | "client_x509_cert_url": ,
91 | "universe_domain":
92 | }
93 | ```
94 |
95 | #### 11. Run the backend server.
96 | ```sh
97 | $ npm start
98 | ```
99 |
100 | #### 12. Open another new VS code terminal and go to frontend folder.
101 | ```sh
102 | $ cd frontend
103 | $ npm install
104 | ```
105 |
106 | #### 13. Create .env file and fill up the credentials (still inside frontend folder).
107 | ```
108 | # REACT API - BACKEND
109 | REACT_APP_API=http://localhost:4000
110 |
111 | # FIREBASE SSO CRENDENTIAL
112 | # (You can get firebase credentials in the project settings also but in "General" tab only. Scroll down and click the npm input radio button and copy the firebaseConfig variable value and put it in this .env file)
113 | REACT_APP_SSO_FIREBASE_API_KEY=
114 | REACT_APP_SSO_FIREBASE_AUTH_DOMAIN=
115 | REACT_APP_SSO_FIREBASE_PROJECT_ID=
116 | REACT_APP_SSO_FIREBASE_STORAGE_BUCKET=
117 | REACT_APP_SSO_FIREBASE_MESSAGING_SENDER_ID=
118 | REACT_APP_SSO_FIREBASE_APP_ID=
119 | REACT_APP_SSO_FIREBASE_MEASUREMENT_ID=
120 |
121 | # GOOGLE IDENTITY SERVICES SSO CRENDENTIAL
122 | # (Follow this tutorial on how to create GIS Client ID - [https://youtu.be/HtJKUQXmtok?si=gwlFBdj-vIt-XoSR])
123 | REACT_APP_SSO_GOOGLE_IDENITY_SERVICES_CLIENT_ID=
124 | ```
125 | Image where to get the firebase credential in the firebase project settings.
126 | 
127 |
128 | #### 14. Run the frontend server.
129 |
130 | ```sh
131 | $ npm start
132 | ```
133 |
134 | ### Features:
135 | - IMPLEMENTED STRONG SECURITY MEASURES
136 | - ACCOUNT ACTIVATION VIA EMAIL
137 | - TWO/MULTI FACTOR AUTHENTICATION VERIFICATION LOGIN CODE
138 | - FIREBASE FACEBOOK SSO OAuth 2.0
139 | - GOOGLE IDENTITY SERVICES SSO OAuth 2.0
140 | - GOOGLE IDENTITY SERVICES ONE TAP PROMPT OAuth 2.0
141 | - FORGOT PASSWORD, RECOVERY ACCOUNT RESET PASSWORD
142 | - CUSTOMIZABLE AUTHENTICATION SUCH AS COOKIES, JWT, AND USER SETTINGS
143 | - ERROR CODES GUIDELINES
144 | - CUSTOMIZABLE EMAIL TEMPLATES
145 | - SCALED NODEJS SERVER WITH AUTOMATICALLY HANDLE LOAD BALANCING
146 |
147 | ### Security Measures Implemented (Basic-Advanced):
148 | - XSS ATTACK
149 | - NOSQL INJECTION ATTACK
150 | - SQL INJECTION ATTACK (AND OTHER CODE INJECTION ATTACKS)
151 | - DOS ATTACK
152 | - DDOS ATTACK
153 | - BRUTE-FORCE ATTACKS
154 | - CSRF ATTACK
155 | - RCE ATTACK
156 | - COOKIE POISONING ATTACK
157 | - UI REDRESS ATTACK
158 | - TOKEN LEAKAGE
159 | - TOKEN TAMPERING ATTACK
160 | - TOKEN REPLAY
161 | - PASSWORD HACKING
162 | - INFORMATION LEAKAGE
163 |
164 | #### More images:
165 |
166 | 
167 |
168 | 
169 |
170 | 
171 |
172 | 
173 |
174 | 
175 |
176 | 
--------------------------------------------------------------------------------
/frontend/src/pages/Public/ForgotPassword/ForgotPassword.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Formik, Form, ErrorMessage } from 'formik';
3 | import { escape } from 'he';
4 | import * as Yup from 'yup';
5 | import DOMPurify from 'dompurify'; // FOR SANITIZING USER INPUT TO PREVENT XSS ATTACKS BEFORE SENDING TO THE BACKEND
6 | import axios from 'axios';
7 | import style from './ForgotPassword.module.css';
8 | import logo from '../../../assets/logo-header.png';
9 |
10 | import { useSelector, useDispatch } from 'react-redux';
11 | import { setDisable, setNotDisable, hasError, hasNoError } from '../../../actions';
12 | import { AllReducers } from '../../../interfaces';
13 |
14 | import CustomButton from '../../../components/AllRoutes/CustomButton/CustomButton';
15 | import CustomAlert from '../../../components/AllRoutes/CustomAlert/CustomAlert';
16 | import CustomInput from '../../../components/AllRoutes/CustomInput/CustomInput';
17 | import CustomLink from '../../../components/AllRoutes/CustomLink/CustomLink';
18 | import Layout from '../../../components/AllRoutes/Layout/Layout';
19 |
20 | type valuesType = {
21 | email: string
22 | }
23 |
24 | const ForgotPassword = () => {
25 | const [isUserAccountRecoveryResetPasswordEmailSent, setIsUserAccountRecoveryResetPasswordEmailSent] = useState(false);
26 | const error = useSelector((state: AllReducers) => state.error);
27 | const dispatch = useDispatch();
28 |
29 | const initialValues = {
30 | email: ''
31 | };
32 |
33 | const validationSchema = Yup.object().shape({
34 | email: Yup.string()
35 | .required('Email is required')
36 | .trim()
37 | .matches(
38 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
39 | 'Please enter a valid email address'
40 | )
41 | .email('Please enter a valid email address')
42 | .test(
43 | 'email-xss-nosql',
44 | 'Invalid email format or potentially unsafe characters',
45 | (value) => {
46 | const sanitizedValue = escape(value);
47 | return sanitizedValue === value; // Check if sanitized value is the same as the original value
48 | }
49 | )
50 | });
51 |
52 | const handleSubmit = (values: valuesType) => {
53 | const {email} = values;
54 | let sanitizedRegisterEmail = DOMPurify.sanitize(email);
55 | dispatch(setDisable());
56 | axios.post(`${process.env.REACT_APP_API}/api/v1/authentication/forgot-password`, {
57 | email: sanitizedRegisterEmail
58 | })
59 | .then((response) => {
60 | if(response.status === 200 && response.data.status === 'ok') {
61 | dispatch(setNotDisable());
62 | if(error.hasError) {
63 | dispatch(hasNoError());
64 | }
65 | setIsUserAccountRecoveryResetPasswordEmailSent(true);
66 | }
67 | })
68 | .catch(function (error) {
69 | dispatch(setNotDisable());
70 | dispatch(hasError(error.response.data.message));
71 | });
72 | };
73 |
74 |
75 | if(isUserAccountRecoveryResetPasswordEmailSent) {
76 | return (
77 | <>
78 |
79 |
90 |
91 |
92 |
93 |
94 |
Recovery Account Email Sent
95 |
Email has been sent to recover your account by updating your password.
96 |
Go back to login page
97 |
98 |
99 |
100 | >
101 | )
102 | }
103 |
104 | return (
105 | <>
106 |
107 |
118 |
119 |
120 |
121 |
136 |
137 |
138 | >
139 | )
140 | }
141 |
142 | export default ForgotPassword;
--------------------------------------------------------------------------------
/backend/src/server.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config();
3 | import express from 'express';
4 | import http from 'http';
5 | import mongoose from 'mongoose';
6 | import cors from 'cors';
7 | import cookieParser from 'cookie-parser';
8 | import mongoSanitize from 'express-mongo-sanitize';
9 | import helmet from 'helmet';
10 | import cluster from 'cluster';
11 | import os from 'os';
12 | import colors from 'colors';
13 |
14 | const numCPUs = os.cpus().length;
15 |
16 | // * ------------ ROUTERS --------------------
17 | import v1AuthenticationRouter from './routes/v1AuthenticationRouter.js';
18 | // * ------------ ROUTERS --------------------
19 |
20 | // * ------------ MIDDLEWARES --------------------
21 | import errorHandler from './middlewares/errorHandler.js';
22 | // * ------------ MIDDLEWARES --------------------
23 |
24 | mongoose.set('strictQuery', false);
25 |
26 | const app = express();
27 |
28 | app.use(helmet({
29 | dnsPrefetchControl: {
30 | allow: false,
31 | },
32 | frameguard: {
33 | action: "deny",
34 | },
35 | hidePoweredBy: true,
36 | noSniff: true,
37 | referrerPolicy: {
38 | policy: ["origin"]
39 | },
40 | xssFilter: true,
41 | hsts: {
42 | maxAge: 31536000, // * 1 year in seconds
43 | includeSubDomains: true,
44 | preload: true
45 | },
46 | contentSecurityPolicy: {
47 | directives: {
48 | defaultSrc: ["'self'"],
49 | scriptSrc: ["'self'", "'unsafe-inline'"],
50 | styleSrc: ["'self'", "'unsafe-inline'"],
51 | imgSrc: ["'self'"],
52 | connectSrc: ["'self'"],
53 | fontSrc: ["'self'"],
54 | objectSrc: ["'none'"],
55 | mediaSrc: ["'none'"],
56 | frameSrc: ["'none'"]
57 | }
58 | }
59 | }));
60 | app.use(express.json());
61 | app.use(mongoSanitize());
62 | app.use(cookieParser());
63 | app.use(
64 | cors({
65 | origin: ['*', process.env["REACT_URL"] as string],
66 | methods: ['GET', 'POST', 'PUT', 'DELETE'],
67 | credentials: true
68 | })
69 | );
70 |
71 | app.use('/api/v1/authentication', v1AuthenticationRouter);
72 |
73 | app.get("*", (req: express.Request, res: express.Response) => {
74 | res.writeHead(302, {'Location': `${process.env["REACT_URL"] as string}/home`});
75 | res.end();
76 | });
77 |
78 | app.use(errorHandler);
79 | colors.enable();
80 |
81 | if (process.env["NODE_ENV"] as string === "PRODUCTION") {
82 | const server = http.createServer(app);
83 |
84 | if(cluster.isPrimary) {
85 | const styles: string[] = [
86 | "font-size: 12px",
87 | "font-family: monospace",
88 | "background: white",
89 | "display: inline-block",
90 | "color: black",
91 | "padding: 8px 19px",
92 | "border: 1px dashed"
93 | ];
94 | console.clear();
95 | console.log(`
96 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⢤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
97 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠟⠃⠀⠀⠙⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀
98 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠋⠀⠀⠀⠀⠀⠀⠘⣆⠀⠀⠀⠀⠀⠀⠀⠀
99 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠾⢛⠒⠀⠀⠀⠀⠀⠀⠀⢸⡆⠀⠀⠀⠀⠀⠀⠀
100 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣶⣄⡈⠓⢄⠠⡀⠀⠀⠀⣄⣷⠀⠀⠀⠀⠀⠀⠀
101 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣷⠀⠈⠱⡄⠑⣌⠆⠀⠀⡜⢻⠀⠀⠀⠀⠀⠀⠀
102 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡿⠳⡆⠐⢿⣆⠈⢿⠀⠀⡇⠘⡆⠀⠀⠀⠀⠀⠀
103 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣷⡇⠀⠀⠈⢆⠈⠆⢸⠀⠀⢣⠀⠀⠀⠀⠀⠀
104 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣧⠀⠀⠈⢂⠀⡇⠀⠀⢨⠓⣄⠀⠀⠀⠀
105 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⣦⣤⠖⡏⡸⠀⣀⡴⠋⠀⠈⠢⡀⠀⠀
106 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⠁⣹⣿⣿⣿⣷⣾⠽⠖⠊⢹⣀⠄⠀⠀⠀⠈⢣⡀
107 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡟⣇⣰⢫⢻⢉⠉⠀⣿⡆⠀⠀⡸⡏⠀⠀⠀⠀⠀⠀⢇
108 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢨⡇⡇⠈⢸⢸⢸⠀⠀⡇⡇⠀⠀⠁⠻⡄⡠⠂⠀⠀⠀⠘
109 | ⢤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠛⠓⡇⠀⠸⡆⢸⠀⢠⣿⠀⠀⠀⠀⣰⣿⣵⡆⠀⠀⠀⠀
110 | ⠈⢻⣷⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡿⣦⣀⡇⠀⢧⡇⠀⠀⢺⡟⠀⠀⠀⢰⠉⣰⠟⠊⣠⠂⠀⡸
111 | ⠀⠀⢻⣿⣿⣷⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⢧⡙⠺⠿⡇⠀⠘⠇⠀⠀⢸⣧⠀⠀⢠⠃⣾⣌⠉⠩⠭⠍⣉⡇
112 | ⠀⠀⠀⠻⣿⣿⣿⣿⣿⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣞⣋⠀⠈⠀⡳⣧⠀⠀⠀⠀⠀⢸⡏⠀⠀⡞⢰⠉⠉⠉⠉⠉⠓⢻⠃
113 | ⠀⠀⠀⠀⠹⣿⣿⣿⣿⣿⣿⣷⡄⠀⠀⢀⣀⠠⠤⣤⣤⠤⠞⠓⢠⠈⡆⠀⢣⣸⣾⠆⠀⠀⠀⠀⠀⢀⣀⡼⠁⡿⠈⣉⣉⣒⡒⠢⡼⠀
114 | ⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⣎⣽⣶⣤⡶⢋⣤⠃⣠⡦⢀⡼⢦⣾⡤⠚⣟⣁⣀⣀⣀⣀⠀⣀⣈⣀⣠⣾⣅⠀⠑⠂⠤⠌⣩⡇⠀
115 | ⠀⠀⠀⠀⠀⠀⠘⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡁⣺⢁⣞⣉⡴⠟⡀⠀⠀⠀⠁⠸⡅⠀⠈⢷⠈⠏⠙⠀⢹⡛⠀⢉⠀⠀⠀⣀⣀⣼⡇⠀
116 | ⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣽⣿⡟⢡⠖⣡⡴⠂⣀⣀⣀⣰⣁⣀⣀⣸⠀⠀⠀⠀⠈⠁⠀⠀⠈⠀⣠⠜⠋⣠⠁⠀
117 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢿⣿⣿⣿⡟⢿⣿⣿⣷⡟⢋⣥⣖⣉⠀⠈⢁⡀⠤⠚⠿⣷⡦⢀⣠⣀⠢⣄⣀⡠⠔⠋⠁⠀⣼⠃⠀⠀
118 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣿⣿⡄⠈⠻⣿⣿⢿⣛⣩⠤⠒⠉⠁⠀⠀⠀⠀⠀⠉⠒⢤⡀⠉⠁⠀⠀⠀⠀⠀⢀⡿⠀⠀⠀
119 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⢿⣤⣤⠴⠟⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠤⠀⠀⠀⠀⠀⢩⠇⠀⠀⠀
120 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
121 | `);
122 | console.log("%cHi 👋! The server is now running! \n".green, styles.join(";"));
123 | console.log(`Master ${process.pid} is now running`.yellow);
124 | console.log(`Workers:`.magenta);
125 |
126 | for(let i = 0; i < numCPUs; i++) {
127 | cluster.fork();
128 | }
129 | }else {
130 | mongoose.connect(process.env["MONGO_DB_URI"] as string)
131 | .then(() => {
132 | server.listen(process.env["PORT"] as string, () => {
133 | console.log(`Worker ${process.pid} is now started and listening on PORT ${process.env["PORT"] as string}`);
134 | });
135 | })
136 | .catch((error) => {
137 | console.log(`File: server.js - ${error}`);
138 | mongoose.disconnect();
139 | });
140 | }
141 | }else {
142 | const server = http.createServer(app);
143 |
144 | const styles: string[] = [
145 | "font-size: 12px",
146 | "font-family: monospace",
147 | "background: white",
148 | "display: inline-block",
149 | "color: black",
150 | "padding: 8px 19px",
151 | "border: 1px dashed"
152 | ];
153 | console.clear();
154 | console.log(`
155 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡠⢤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
156 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠟⠃⠀⠀⠙⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀
157 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠋⠀⠀⠀⠀⠀⠀⠘⣆⠀⠀⠀⠀⠀⠀⠀⠀
158 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠾⢛⠒⠀⠀⠀⠀⠀⠀⠀⢸⡆⠀⠀⠀⠀⠀⠀⠀
159 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣶⣄⡈⠓⢄⠠⡀⠀⠀⠀⣄⣷⠀⠀⠀⠀⠀⠀⠀
160 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣷⠀⠈⠱⡄⠑⣌⠆⠀⠀⡜⢻⠀⠀⠀⠀⠀⠀⠀
161 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡿⠳⡆⠐⢿⣆⠈⢿⠀⠀⡇⠘⡆⠀⠀⠀⠀⠀⠀
162 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣷⡇⠀⠀⠈⢆⠈⠆⢸⠀⠀⢣⠀⠀⠀⠀⠀⠀
163 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣧⠀⠀⠈⢂⠀⡇⠀⠀⢨⠓⣄⠀⠀⠀⠀
164 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⣦⣤⠖⡏⡸⠀⣀⡴⠋⠀⠈⠢⡀⠀⠀
165 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⠁⣹⣿⣿⣿⣷⣾⠽⠖⠊⢹⣀⠄⠀⠀⠀⠈⢣⡀
166 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡟⣇⣰⢫⢻⢉⠉⠀⣿⡆⠀⠀⡸⡏⠀⠀⠀⠀⠀⠀⢇
167 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢨⡇⡇⠈⢸⢸⢸⠀⠀⡇⡇⠀⠀⠁⠻⡄⡠⠂⠀⠀⠀⠘
168 | ⢤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠛⠓⡇⠀⠸⡆⢸⠀⢠⣿⠀⠀⠀⠀⣰⣿⣵⡆⠀⠀⠀⠀
169 | ⠈⢻⣷⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡿⣦⣀⡇⠀⢧⡇⠀⠀⢺⡟⠀⠀⠀⢰⠉⣰⠟⠊⣠⠂⠀⡸
170 | ⠀⠀⢻⣿⣿⣷⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⢧⡙⠺⠿⡇⠀⠘⠇⠀⠀⢸⣧⠀⠀⢠⠃⣾⣌⠉⠩⠭⠍⣉⡇
171 | ⠀⠀⠀⠻⣿⣿⣿⣿⣿⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣞⣋⠀⠈⠀⡳⣧⠀⠀⠀⠀⠀⢸⡏⠀⠀⡞⢰⠉⠉⠉⠉⠉⠓⢻⠃
172 | ⠀⠀⠀⠀⠹⣿⣿⣿⣿⣿⣿⣷⡄⠀⠀⢀⣀⠠⠤⣤⣤⠤⠞⠓⢠⠈⡆⠀⢣⣸⣾⠆⠀⠀⠀⠀⠀⢀⣀⡼⠁⡿⠈⣉⣉⣒⡒⠢⡼⠀
173 | ⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⣎⣽⣶⣤⡶⢋⣤⠃⣠⡦⢀⡼⢦⣾⡤⠚⣟⣁⣀⣀⣀⣀⠀⣀⣈⣀⣠⣾⣅⠀⠑⠂⠤⠌⣩⡇⠀
174 | ⠀⠀⠀⠀⠀⠀⠘⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡁⣺⢁⣞⣉⡴⠟⡀⠀⠀⠀⠁⠸⡅⠀⠈⢷⠈⠏⠙⠀⢹⡛⠀⢉⠀⠀⠀⣀⣀⣼⡇⠀
175 | ⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣽⣿⡟⢡⠖⣡⡴⠂⣀⣀⣀⣰⣁⣀⣀⣸⠀⠀⠀⠀⠈⠁⠀⠀⠈⠀⣠⠜⠋⣠⠁⠀
176 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢿⣿⣿⣿⡟⢿⣿⣿⣷⡟⢋⣥⣖⣉⠀⠈⢁⡀⠤⠚⠿⣷⡦⢀⣠⣀⠢⣄⣀⡠⠔⠋⠁⠀⣼⠃⠀⠀
177 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣿⣿⡄⠈⠻⣿⣿⢿⣛⣩⠤⠒⠉⠁⠀⠀⠀⠀⠀⠉⠒⢤⡀⠉⠁⠀⠀⠀⠀⠀⢀⡿⠀⠀⠀
178 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⢿⣤⣤⠴⠟⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠤⠀⠀⠀⠀⠀⢩⠇⠀⠀⠀
179 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
180 | `);
181 | console.log("%cHi 👋! The server is now running! \n".green, styles.join(";"));
182 |
183 | mongoose.connect(process.env["MONGO_DB_URI"] as string)
184 | .then(() => {
185 | server.listen(process.env["PORT"] as string, () => {
186 | console.log(`Worker ${process.pid} is now started and listening on PORT ${process.env["PORT"] as string}`);
187 | });
188 | })
189 | .catch((error) => {
190 | console.log(`File: server.js - ${error}`);
191 | mongoose.disconnect();
192 | });
193 | }
--------------------------------------------------------------------------------
/frontend/src/pages/Public/Login/Login.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from 'react-router-dom';
2 | import { Formik, Form, ErrorMessage } from 'formik';
3 | import { escape } from 'he';
4 | import * as Yup from 'yup';
5 | import DOMPurify from 'dompurify'; // FOR SANITIZING USER INPUT TO PREVENT XSS ATTACKS BEFORE SENDING TO THE BACKEND
6 | import axios from 'axios';
7 | import style from './Login.module.css';
8 | import FirebaseGoogleSignInButton from '../../../components/Public/FirebaseGoogleSignInButton/FirebaseGoogleSignInButton';
9 | import FirebaseFacebookSignInButton from '../../../components/Public/FirebaseFacebookSignInButton/FirebaseFacebookSignInButton';
10 | import GoogleIdentityServices from '../../../components/Public/GoogleIdentityServices/GoogleIdentityServices';
11 | import logo from '../../../assets/logo-header.png';
12 |
13 | import { useSelector, useDispatch } from 'react-redux';
14 | import { setDisable, setNotDisable, hasError, hasNoError } from '../../../actions';
15 | import { AllReducers } from '../../../interfaces';
16 |
17 | import CustomButton from '../../../components/AllRoutes/CustomButton/CustomButton';
18 | import CustomAlert from '../../../components/AllRoutes/CustomAlert/CustomAlert';
19 | import CustomInput from '../../../components/AllRoutes/CustomInput/CustomInput';
20 | import CustomLink from '../../../components/AllRoutes/CustomLink/CustomLink';
21 | import Layout from '../../../components/AllRoutes/Layout/Layout';
22 |
23 | type valuesType = {
24 | username: string,
25 | password: string
26 | }
27 |
28 | const Login = () => {
29 | const navigate = useNavigate();
30 | const error = useSelector((state: AllReducers) => state.error);
31 | const dispatch = useDispatch();
32 |
33 | const initialValues = {
34 | username: '',
35 | password: ''
36 | };
37 |
38 | const validationSchema = Yup.object().shape({
39 | username: Yup.string()
40 | .required('Username is required')
41 | .trim()
42 | .min(4, 'Username must be at least 4 characters')
43 | .max(20, 'Username must not exceed 20 characters')
44 | .matches(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores')
45 | .test(
46 | 'username-security',
47 | 'Username should not contain sensitive information',
48 | (value) => !/\b(admin|root|superuser)\b/i.test(value)
49 | )
50 | .test(
51 | 'username-xss-nosql',
52 | 'Invalid characters detected',
53 | (value) => {
54 | const sanitizedValue = escape(value);
55 | return sanitizedValue === value; // Check if sanitized value is the same as the original value
56 | }
57 | ),
58 | password: Yup.string()
59 | .required('Password is required')
60 | .min(12, 'Password must be at least 12 characters')
61 | .matches(
62 | /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()\-_=+{};:,<.>]).+$/,
63 | 'Password must contain at least one uppercase letter, one lowercase letter, one digit, and one special character'
64 | )
65 | .test(
66 | 'password-security',
67 | 'Password should not be commonly used or easily guessable',
68 | (value) => !/\b(password|123456789)\b/i.test(value)
69 | )
70 | });
71 |
72 | const handleSubmit = (values:valuesType) => {
73 | const {username, password} = values;
74 | const sanitizedLoginUsername = DOMPurify.sanitize(username);
75 | const sanitizedLoginPassword = DOMPurify.sanitize(password);
76 | dispatch(setDisable());
77 | axios.post(`${process.env.REACT_APP_API}/api/v1/authentication/login`, {
78 | username: sanitizedLoginUsername,
79 | password: sanitizedLoginPassword
80 | })
81 | .then((response) => {
82 | if(response.status === 200 && response.data.status === 'ok') {
83 | dispatch(setNotDisable());
84 | if(error.hasError) {
85 | dispatch(hasNoError());
86 | }
87 | navigate('/login/multi-factor-authentication');
88 | }
89 | })
90 | .catch(function (error) {
91 | dispatch(setNotDisable());
92 | dispatch(hasError(error.response.data.message));
93 | });
94 | };
95 |
96 | return (
97 | <>
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Register
107 |
108 |
109 |
110 |
111 |
139 |
140 |
141 | >
142 | )
143 | }
144 |
145 | export default Login;
--------------------------------------------------------------------------------
/frontend/src/pages/Public/ResetPassword/ResetPassword.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useParams, useNavigate } from "react-router-dom";
3 | import { Formik, Form, ErrorMessage } from 'formik';
4 | import * as Yup from 'yup';
5 | import DOMPurify from 'dompurify'; // FOR SANITIZING USER INPUT TO PREVENT XSS ATTACKS BEFORE SENDING TO THE BACKEND
6 | import axios from 'axios';
7 | import style from './ResetPassword.module.css';
8 | import logo from '../../../assets/logo-header.png';
9 |
10 | import { useSelector, useDispatch } from 'react-redux';
11 | import { setDisable, setNotDisable, hasError, hasNoError } from '../../../actions';
12 | import { AllReducers } from '../../../interfaces';
13 |
14 | import CustomButton from '../../../components/AllRoutes/CustomButton/CustomButton';
15 | import CustomAlert from '../../../components/AllRoutes/CustomAlert/CustomAlert';
16 | import CustomInput from '../../../components/AllRoutes/CustomInput/CustomInput';
17 | import Layout from '../../../components/AllRoutes/Layout/Layout';
18 |
19 | type valuesType = {
20 | password: string,
21 | repeatPassword: string
22 | }
23 |
24 | const ResetPassword = () => {
25 | const navigate = useNavigate();
26 | const { token, csrfToken } = useParams();
27 | const [isAccountRecoveryResetPasswordTokenValid, setIsAccountRecoveryResetPasswordTokenValid] = useState(false);
28 | const error = useSelector((state: AllReducers) => state.error);
29 | const dispatch = useDispatch();
30 |
31 | const initialValues = {
32 | password: '',
33 | repeatPassword: ''
34 | };
35 |
36 | const validationSchema = Yup.object().shape({
37 | password: Yup.string()
38 | .required('Password is required')
39 | .min(12, 'Password must be at least 12 characters')
40 | .matches(
41 | /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()\-_=+{};:,<.>]).+$/,
42 | 'Password must contain at least one uppercase letter, one lowercase letter, one digit, and one special character'
43 | )
44 | .test(
45 | 'password-security',
46 | 'Password should not be commonly used or easily guessable',
47 | (value) => !/\b(password|123456789)\b/i.test(value)
48 | ),
49 | repeatPassword: Yup.string()
50 | .oneOf([Yup.ref('password')], 'Passwords must match')
51 | .required('Please repeat your password')
52 | });
53 |
54 | const handleSubmit = (values: valuesType) => {
55 | const {password, repeatPassword} = values;
56 | let sanitizedreset_passwordPassword = DOMPurify.sanitize(password);
57 | let sanitizedreset_passwordRepeatPassword = DOMPurify.sanitize(repeatPassword);
58 | dispatch(setDisable());
59 | axios.post(`${process.env.REACT_APP_API}/api/v1/authentication/reset-password`, {
60 | token: token,
61 | csrfToken: csrfToken,
62 | password: sanitizedreset_passwordPassword,
63 | repeatPassword: sanitizedreset_passwordRepeatPassword
64 | })
65 | .then((response) => {
66 | if(response.status === 200 && response.data.status === 'ok') {
67 | dispatch(setNotDisable());
68 | if(error.hasError) {
69 | dispatch(hasNoError());
70 | }
71 | navigate('/login');
72 | }
73 | })
74 | .catch(function (error) {
75 | dispatch(setNotDisable());
76 | dispatch(hasError(error.response.data.message));
77 | });
78 | };
79 |
80 | useEffect(() => {
81 | if(token !== null) {
82 | axios.post(`${process.env.REACT_APP_API}/api/v1/authentication/account-recovery/reset-password/verify-token`, {
83 | token: token,
84 | csrfToken: csrfToken
85 | })
86 | .then((response) => {
87 | if(response.status === 200 && response.data.status === 'ok') {
88 | dispatch(setNotDisable());
89 | setIsAccountRecoveryResetPasswordTokenValid(true);
90 | }
91 | })
92 | .catch(function (error) {
93 | dispatch(setNotDisable());
94 | dispatch(hasError(error.response.data.message));
95 | navigate('/forgot-password');
96 | });
97 | }else {
98 | dispatch(setNotDisable());
99 | navigate('/');
100 | }
101 | // eslint-disable-next-line react-hooks/exhaustive-deps
102 | }, []);
103 |
104 |
105 | if(!isAccountRecoveryResetPasswordTokenValid) {
106 | return (
107 | <>
108 |
109 |
110 |
111 |

112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
Loading...
121 |
Verifying may take a while. Please wait.
122 |
123 |
124 |
125 | >
126 | )
127 | }
128 |
129 | return (
130 | <>
131 |
132 |
133 |
134 |

135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
160 |
161 |
162 | >
163 | )
164 | }
165 |
166 | export default ResetPassword;
--------------------------------------------------------------------------------
/backend/src/middlewares/v1AuthenticationLimiter.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config();
3 | import rateLimit from 'express-rate-limit';
4 | // @ts-ignore
5 | import MongoStore from 'rate-limit-mongo';
6 |
7 | export const userLimiter = rateLimit({
8 | store: new MongoStore({
9 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
10 | collectionName: 'user-limits', // * MongoDB collection to store rate limit data
11 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
12 | errorHandler: console.error, // * Optional error handler
13 | }),
14 | max: 100, // * Maximum number of requests per time window
15 | message: 'Too many user requests, Please try again later.',
16 | });
17 |
18 | export const loginLimiter = rateLimit({
19 | store: new MongoStore({
20 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
21 | collectionName: 'login-limits', // * MongoDB collection to store rate limit data
22 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
23 | errorHandler: console.error, // * Optional error handler
24 | }),
25 | max: 100, // * Maximum number of requests per time window
26 | message: 'Too many login requests, Please try again later.',
27 | });
28 |
29 | export const verificationCodeLoginLimiter = rateLimit({
30 | store: new MongoStore({
31 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
32 | collectionName: 'verification-code-login-limits', // * MongoDB collection to store rate limit data
33 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
34 | errorHandler: console.error, // * Optional error handler
35 | }),
36 | max: 100, // * Maximum number of requests per time window
37 | message: 'Too many verification code login requests, Please try again later.',
38 | });
39 |
40 | export const verificationCodeLoginLogoutLimiter = rateLimit({
41 | store: new MongoStore({
42 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
43 | collectionName: 'verification-code-login-logout-limits', // * MongoDB collection to store rate limit data
44 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
45 | errorHandler: console.error, // * Optional error handler
46 | }),
47 | max: 100, // * Maximum number of requests per time window
48 | message: 'Too many verification code login logout requests, Please try again later.',
49 | });
50 |
51 | export const registerLimiter = rateLimit({
52 | store: new MongoStore({
53 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
54 | collectionName: 'register-limits', // * MongoDB collection to store rate limit data
55 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
56 | errorHandler: console.error, // * Optional error handler
57 | }),
58 | max: 100, // * Maximum number of requests per time window
59 | message: 'Too many register requests, Please try again later.',
60 | });
61 |
62 | export const activateLimiter = rateLimit({
63 | store: new MongoStore({
64 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
65 | collectionName: 'activate-limits', // * MongoDB collection to store rate limit data
66 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
67 | errorHandler: console.error, // * Optional error handler
68 | }),
69 | max: 100, // * Maximum number of requests per time window
70 | message: 'Too many activate requests, Please try again later.',
71 | });
72 |
73 | export const forgotPasswordLimiter = rateLimit({
74 | store: new MongoStore({
75 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
76 | collectionName: 'forgot-password-limits', // * MongoDB collection to store rate limit data
77 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
78 | errorHandler: console.error, // * Optional error handler
79 | }),
80 | max: 100, // * Maximum number of requests per time window
81 | message: 'Too many forgot password requests, Please try again later.',
82 | });
83 |
84 | export const resetPasswordLimiter = rateLimit({
85 | store: new MongoStore({
86 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
87 | collectionName: 'reset-password-limits', // * MongoDB collection to store rate limit data
88 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
89 | errorHandler: console.error, // * Optional error handler
90 | }),
91 | max: 100, // * Maximum number of requests per time window
92 | message: 'Too many reset password requests, Please try again later.',
93 | });
94 |
95 | export const resetPasswordVerifyTokenLimiter = rateLimit({
96 | store: new MongoStore({
97 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
98 | collectionName: 'reset-password-verify-token-limits', // * MongoDB collection to store rate limit data
99 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
100 | errorHandler: console.error, // * Optional error handler
101 | }),
102 | max: 100, // * Maximum number of requests per time window
103 | message: 'Too many reset password verify token requests, Please try again later.',
104 | });
105 |
106 | export const deleteUserLimiter = rateLimit({
107 | store: new MongoStore({
108 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
109 | collectionName: 'delete-user-limits', // * MongoDB collection to store rate limit data
110 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
111 | errorHandler: console.error, // * Optional error handler
112 | }),
113 | max: 100, // * Maximum number of requests per time window
114 | message: 'Too many delete user requests, Please try again later.',
115 | });
116 |
117 | export const logoutLimiter = rateLimit({
118 | store: new MongoStore({
119 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
120 | collectionName: 'logout-limits', // * MongoDB collection to store rate limit data
121 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
122 | errorHandler: console.error, // * Optional error handler
123 | }),
124 | max: 100, // * Maximum number of requests per time window
125 | message: 'Too many logout requests, Please try again later.',
126 | });
127 |
128 | export const enableGoogleAuthenticatorLimiter = rateLimit({
129 | store: new MongoStore({
130 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
131 | collectionName: 'enable-google-authenticator-limiter', // * MongoDB collection to store rate limit data
132 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
133 | errorHandler: console.error, // * Optional error handler
134 | }),
135 | max: 100, // * Maximum number of requests per time window
136 | message: 'Too many enable google authenticator requests, Please try again later.',
137 | });
138 |
139 | export const activateGoogleAuthenticatorLimiter = rateLimit({
140 | store: new MongoStore({
141 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
142 | collectionName: 'activate-google-authenticator-limiter', // * MongoDB collection to store rate limit data
143 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
144 | errorHandler: console.error, // * Optional error handler
145 | }),
146 | max: 100, // * Maximum number of requests per time window
147 | message: 'Too many activate google authenticator requests, Please try again later.',
148 | });
149 |
150 | export const disableGoogleAuthenticatorLimiter = rateLimit({
151 | store: new MongoStore({
152 | uri: process.env["MONGO_DB_URI_LIMITER"] as string, // * MongoDB connection URI
153 | collectionName: 'disable-google-authenticator-limiter', // * MongoDB collection to store rate limit data
154 | expireTimeMs: 60 * 1000, // * Time window in milliseconds
155 | errorHandler: console.error, // * Optional error handler
156 | }),
157 | max: 100, // * Maximum number of requests per time window
158 | message: 'Too many disable google authenticator requests, Please try again later.',
159 | });
--------------------------------------------------------------------------------
/frontend/src/components/Private/HeaderDropdown/HeaderDropdown.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Menu, Transition } from '@headlessui/react';
3 | import { Fragment } from 'react';
4 | import { ChevronDownIcon } from '@heroicons/react/20/solid';
5 | import { useOutletContext } from 'react-router-dom';
6 | import GoogleAuthenticationDialog from '../GoogleAuthenticatorDialog/GoogleAuthenticationDialog';
7 |
8 | import CustomButton from '../../AllRoutes/CustomButton/CustomButton';
9 |
10 | interface HeaderDropDownProps {
11 | handleLogout: () => void;
12 | handleDeleteUser: () => void;
13 | }
14 |
15 | export default function HeaderDropdown({ handleLogout, handleDeleteUser }: HeaderDropDownProps) {
16 | const [isOpen, setIsOpen] = useState(false);
17 | const [user]: any = useOutletContext();
18 |
19 | return (
20 | <>
21 | {!user.isSSO && { setIsOpen(false); }} />}
22 |
104 | >
105 | );
106 | }
107 |
108 | function EditInactiveIcon(props: any) {
109 | return (
110 |
123 | )
124 | }
125 |
126 | function EditActiveIcon(props: any) {
127 | return (
128 |
141 | )
142 | }
143 |
144 | function DuplicateInactiveIcon(props: any) {
145 | return (
146 |
165 | )
166 | }
167 |
168 | function DuplicateActiveIcon(props: any) {
169 | return (
170 |
189 | )
190 | }
191 |
192 | function DeleteInactiveIcon(props: any) {
193 | return (
194 |
212 | )
213 | }
214 |
215 | function DeleteActiveIcon(props: any) {
216 | return (
217 |
235 | )
236 | }
237 |
--------------------------------------------------------------------------------
/backend/src/constants/v1AuthenticationErrorCodes.ts:
--------------------------------------------------------------------------------
1 | // * ------------- AUTHENTICATE JWT TOKEN MIDDLEWARE -------------
2 | export const NO_JWT_TOKEN_AUTHENTICATE_JWT_TOKEN: number = 300;
3 | export const INVALID_JWT_TOKEN_AUTHENTICATE_JWT_TOKEN: number = 301;
4 | export const NO_USER_FOUND_IN_DATABASE_INSIDE_JWT_DECODED_TOKEN_AUTHENTICATE_JWT_TOKEN: number = 302;
5 |
6 | // * ------------- VERIFY PRIVATE CSRF TOKEN MIDDLEWARE -------------
7 | export const NO_CSRF_TOKEN_VERIFY_PRIVATE_CSRF_TOKEN: number = 303;
8 | export const INVALID_CSRF_TOKEN_VERIFY_PRIVATE_CSRF_TOKEN: number = 304;
9 |
10 | // * ------------- VERIFY PUBLIC CSRF TOKEN MIDDLEWARE -------------
11 | export const NO_CSRF_TOKEN_VERIFY_PUBLIC_CSRF_TOKEN: number = 305;
12 | export const INVALID_CSRF_TOKEN_VERIFY_PUBLIC_CSRF_TOKEN: number = 306;
13 |
14 | // * ------------- LOGIN CONTROLLER -------------
15 | export const INCOMPLETE_LOGIN_FORM: number = 307;
16 | export const INVALID_USER_INPUT_LOGIN: number = 308;
17 | export const USERNAME_NOT_EXIST_LOGIN: number = 309;
18 | export const PASSWORD_NOT_MATCH_LOGIN: number = 310;
19 | export const USER_SSO_ACCOUNT_LOGIN: number = 311;
20 |
21 | // * ------------- VERIFICATION CODE LOGIN CONTROLLER -------------
22 | export const INCOMPLETE_LOGIN_FORM_VERIFICATION_CODE_LOGIN: number = 312;
23 | export const INVALID_OR_EXPIRED_MULTI_FACTOR_AUTHENTICATION_LOGIN_CODE: number = 313;
24 | export const INVALID_USER_INPUT_VERIFICATION_CODE_LOGIN: number = 314;
25 | export const USER_NOT_EXIST_VERIFICATION_CODE_LOGIN: number = 315;
26 | export const VERIFICATION_CODE_LOGIN_NOT_MATCH: number = 316;
27 | export const EXPIRED_VERIFICATION_CODE_LOGIN: number = 317;
28 |
29 | // * -------------- GOOGLE AUTHENTICATOR CODE LOGIN CONTROLLER ----------------
30 | export const INCOMPLETE_LOGIN_FORM_GOOGLE_AUTHENTICATOR_CODE_LOGIN: number = 318;
31 | export const INVALID_OR_EXPIRED_MULTI_FACTOR_AUTHENTICATION_LOGIN_CODE_GOOGLE_AUTHENTICATOR_CODE_LOGIN: number = 319;
32 | export const INVALID_USER_INPUT_GOOGLE_AUTHENTICATOR_CODE_LOGIN: number = 320;
33 | export const INVALID_GOOGLE_AUTHENTICATOR_CODE_LOGIN: number = 321;
34 |
35 | // * ------------- REGISTER CONTROLLER -------------
36 | export const INCOMPLETE_REGISTER_FORM: number = 322;
37 | export const INVALID_USER_INPUT_REGISTER: number = 323;
38 | export const USERNAME_EXIST_REGISTER: number = 324;
39 | export const EMAIL_EXIST_REGISTER: number = 325;
40 |
41 | // * ------------- ACTIVATE CONTROLLER -------------
42 | export const INCOMPLETE_REGISTER_FORM_ACTIVATE: number = 326;
43 | export const INVALID_USER_INPUT_REGISTER_ACTIVATE: number = 327;
44 | export const USERNAME_EXIST_REGISTER_ACTIVATE: number = 328;
45 | export const EMAIL_EXIST_REGISTER_ACTIVATE: number = 329;
46 | export const EXPIRED_ACCOUNT_ACTIVATION_JWT_TOKEN_OR_INVALID_ACCOUNT_ACTIVATION_JWT_TOKEN: number = 330;
47 | export const NO_ACCOUNT_ACTIVATION_JWT_TOKEN: number = 331;
48 |
49 | // * ------------- FORGOT PASSWORD CONTROLLER -------------
50 | export const INCOMPLETE_FORGOT_PASSWORD_FORM: number = 332;
51 | export const INVALID_USER_INPUT_FORGOT_PASSWORD: number = 333;
52 | export const EMAIL_NOT_EXIST_FORGOT_PASSWORD: number = 334;
53 | export const USER_SSO_ACCOUNT_FORGOT_PASSWORD: number = 335;
54 |
55 | // * ------------- RESET PASSWORD CONTROLLER -------------
56 | export const NO_JWT_TOKEN_OR_CSRF_TOKEN_RESET_PASSWORD: number = 336;
57 | export const EXPIRED_LINK_OR_INVALID_CSRF_TOKEN_RESET_PASSWORD: number = 337;
58 | export const EXPIRED_LINK_OR_INVALID_JWT_TOKEN_RESET_PASSWORD: number = 338;
59 | export const INCOMPLETE_RESET_PASSWORD_FORM: number = 339;
60 | export const PASSWORD_REPEAT_PASSWORD_NOT_MATCH_RESET_PASSWORD_FORM: number = 340;
61 | export const INVALID_USER_INPUT_RESET_PASSWORD: number = 341;
62 | export const EMAIL_NOT_EXIST_RESET_PASSWORD: number = 342;
63 | export const USER_SSO_ACCOUNT_RESET_PASSWORD: number = 343;
64 | export const INVALID_CSRF_TOKEN_RESET_PASSWORD: number = 344;
65 |
66 | // * ------------- ACCOUNT RECOVERY RESET PASSWORD VERIFY TOKEN CONTROLLER -------------
67 | export const NO_JWT_TOKEN_OR_CSRF_TOKEN_ACCOUNT_RECOVERY_RESET_PASSWORD_VERIFY_TOKEN: number = 345;
68 | export const EXPIRED_LINK_OR_INVALID_CSRF_TOKEN_ACCOUNT_RECOVERY_RESET_PASSWORD_VERIFY_TOKEN: number = 346;
69 | export const EXPIRED_LINK_OR_INVALID_JWT_TOKEN_ACCOUNT_RECOVERY_RESET_PASSWORD_VERIFY_TOKEN: number = 347;
70 | export const INCOMPLETE_FORGOT_PASSWORD_FORM_ACCOUNT_RECOVERY_RESET_PASSWORD_VERIFY_TOKEN: number = 348;
71 | export const INVALID_USER_INPUT_FORGOT_PASSWORD_ACCOUNT_RECOVERY_RESET_PASSWORD_VERIFY_TOKEN: number = 349;
72 | export const EMAIL_NOT_EXIST_OR_USER_NOT_REQUEST_FORGOT_PASSWORD_ACCOUNT_RECOVERY_RESET_PASSWORD_VERIFY_TOKEN: number = 350;
73 | export const INVALID_CSRF_TOKEN_ACCOUNT_RECOVERY_RESET_PASSWORD_VERIFY_TOKEN: number = 351;
74 |
75 | // * ------------- SSO SIGN IN GOOGLE IDENTITY SERVICES -------------
76 | export const NO_SSO_JWT_TOKEN_SSO_SIGN_IN_GOOGLE_IDENTITY_SERVICES: number = 352;
77 | export const PAYLOAD_UNDEFINED_SSO_SIGN_IN_GOOGLE_IDENTITY_SERVICES: number = 353;
78 | export const FAILED_VALIDATION_SSO_SIGN_IN_GOOGLE_IDENTITY_SERVICES: number = 354;
79 | export const INCOMPLETE_CREDENTIAL_SSO_SIGN_IN_GOOGLE_IDENTITY_SERVICES: number = 355;
80 | export const INVALID_CREDENTIAL_SSO_SIGN_IN_GOOGLE_IDENTITY_SERVICES: number = 356;
81 | export const USER_NOT_EXIST_SSO_SIGN_IN_GOOGLE_IDENTITY_SERVICES: number = 357;
82 |
83 | // * ------------- SSO SIGN UP GOOGLE IDENTITY SERVICES -------------
84 | export const NO_SSO_JWT_TOKEN_SSO_SIGN_UP_GOOGLE_IDENTITY_SERVICES: number = 358;
85 | export const PAYLOAD_UNDEFINED_SSO_SIGN_UP_GOOGLE_IDENTITY_SERVICES: number = 359;
86 | export const FAILED_VALIDATION_SSO_SIGN_UP_GOOGLE_IDENTITY_SERVICES: number = 360;
87 | export const INCOMPLETE_CREDENTIAL_SSO_SIGN_UP_GOOGLE_IDENTITY_SERVICES: number = 361;
88 | export const INVALID_CREDENTIAL_SSO_SIGN_UP_GOOGLE_IDENTITY_SERVICES: number = 362;
89 | export const USER_ALREADY_EXIST_SSO_SIGN_UP_GOOGLE_IDENTITY_SERVICES: number = 363;
90 |
91 | // * ------------- SSO SIGN IN FIREBASE FACEBOOK -------------
92 | export const NO_SSO_JWT_TOKEN_SSO_SIGN_IN_FIREBASE_FACEBOOK: number = 364;
93 | export const FAILED_DELETE_ACCOUNT_IN_FIREBASE_DATABASE_SSO_SIGN_IN_FIREBASE_FACEBOOK: number = 365;
94 | export const INCOMPLETE_CREDENTIAL_SSO_SIGN_IN_FIREBASE_FACEBOOK: number = 366;
95 | export const INVALID_CREDENTIAL_SSO_SIGN_IN_FIREBASE_FACEBOOK: number = 367;
96 | export const USER_NOT_EXIST_SSO_SIGN_IN_FIREBASE_FACEBOOK: number = 368;
97 |
98 | // * ------------- SSO SIGN UP FIREBASE FACEBOOK -------------
99 | export const NO_SSO_JWT_TOKEN_SSO_SIGN_UP_FIREBASE_FACEBOOK: number = 369;
100 | export const FAILED_DELETE_ACCOUNT_IN_FIREBASE_DATABASE_SSO_SIGN_UP_FIREBASE_FACEBOOK: number = 370;
101 | export const INCOMPLETE_CREDENTIAL_SSO_SIGN_UP_FIREBASE_FACEBOOK: number = 371;
102 | export const INVALID_CREDENTIAL_SSO_SIGN_UP_FIREBASE_FACEBOOK: number = 372;
103 | export const USER_ALREADY_EXIST_SSO_SIGN_UP_FIREBASE_FACEBOOK: number = 373;
104 |
105 | // * ------------- SSO SIGN IN FIREBASE GOOGLE -------------
106 | export const NO_SSO_JWT_TOKEN_SSO_SIGN_IN_FIREBASE_GOOGLE: number = 374;
107 | export const FAILED_DELETE_ACCOUNT_IN_FIREBASE_DATABASE_SSO_SIGN_IN_FIREBASE_GOOGLE: number = 375;
108 | export const INCOMPLETE_CREDENTIAL_SSO_SIGN_IN_FIREBASE_GOOGLE: number = 376;
109 | export const EMAIL_NOT_VERIFIED_SSO_SIGN_IN_FIREBASE_GOOGLE: number = 377;
110 | export const INVALID_CREDENTIAL_SSO_SIGN_IN_FIREBASE_GOOGLE: number = 378;
111 | export const USER_NOT_EXIST_SSO_SIGN_IN_FIREBASE_GOOGLE: number = 379;
112 |
113 | // * ------------- SSO SIGN UP FIREBASE GOOGLE -------------
114 | export const NO_SSO_JWT_TOKEN_SSO_SIGN_UP_FIREBASE_GOOGLE: number = 380;
115 | export const FAILED_DELETE_ACCOUNT_IN_FIREBASE_DATABASE_SSO_SIGN_UP_FIREBASE_GOOGLE: number = 381;
116 | export const INCOMPLETE_CREDENTIAL_SSO_SIGN_UP_FIREBASE_GOOGLE: number = 382;
117 | export const EMAIL_NOT_VERIFIED_SSO_SIGN_UP_FIREBASE_GOOGLE: number = 383;
118 | export const INVALID_CREDENTIAL_SSO_SIGN_UP_FIREBASE_GOOGLE: number = 384;
119 | export const USER_ALREADY_EXIST_SSO_SIGN_UP_FIREBASE_GOOGLE: number = 385;
120 |
121 | // * ------------- DELETE USER CONTROLLER -------------
122 | export const USER_NOT_EXIST_DELETE_USER: number = 386;
123 | export const FAILED_DELETE_ACCOUNT_IN_FIREBASE_DATABASE_DELETE_USER: number = 387;
124 |
125 | // * ------------- MODEL MONGOOSE VALIDATION ERROR -------------
126 | export const MONGOOSE_VALIDATION_ERROR: number = 499;
127 |
128 | // * ------------- SERVER ERROR -------------
129 | export const SERVER_ERROR: number = 500;
130 |
131 |
--------------------------------------------------------------------------------
/backend/src/middlewares/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import jwt from 'jsonwebtoken';
3 | import Tokens from 'csrf';
4 | import lodash from 'lodash';
5 |
6 | // * ------------ MODELS --------------------
7 | import User from "../models/userModel.js";
8 | // * ------------ MODELS --------------------
9 |
10 | // * ------------ CONSTANTS --------------------
11 | import * as cookiesSettings from '../constants/v1AuthenticationCookiesSettings.js'; // * ALL COOKIES SETTINGS
12 | import * as errorCodes from '../constants/v1AuthenticationErrorCodes.js'; // * ALL ERROR CODES
13 | // * ------------ CONSTANTS --------------------
14 |
15 | // * ------------ TYPES --------------------
16 | import * as TYPES from '../types/index.js';
17 | // * ------------ TYPES --------------------
18 |
19 | export function isMFAMode(req: express.Request, res: express.Response, next: express.NextFunction): any {
20 | const MFA_LOGIN_TOKEN: string = req.cookies[cookiesSettings.COOKIE_MFA_TOKEN_NAME];
21 |
22 | if (MFA_LOGIN_TOKEN) {
23 | const MFA_TOKEN_SECRET: string = process.env["MFA_TOKEN_SECRET"] as string;
24 | try {
25 | if (jwt.verify(MFA_LOGIN_TOKEN as string, MFA_TOKEN_SECRET)) {
26 | const { username, profilePicture, hasGoogleAuthenticator }: TYPES.MFA_LOGIN_TOKEN = jwt.decode(MFA_LOGIN_TOKEN) as TYPES.MFA_LOGIN_TOKEN;
27 |
28 | return res.status(200).json({
29 | status: 'MFA-Mode',
30 | user: {
31 | username,
32 | profilePicture,
33 | hasGoogleAuthenticator
34 | }
35 | });
36 | }
37 | } catch (error) {
38 | // * Handle verification error
39 | }
40 | }
41 |
42 | next();
43 | }
44 |
45 | export function isAuthenticated(req: express.Request, res: express.Response, next: express.NextFunction): any {
46 | const authenticationToken: string = req.cookies[cookiesSettings.COOKIE_AUTHENTICATION_TOKEN_NAME];
47 | const csrfToken:any = req.cookies[cookiesSettings.COOKIE_CSRF_TOKEN_NAME];
48 | const tokens = new Tokens();
49 |
50 | if (authenticationToken == null) {
51 | // NOT AUTHENTICATED USER
52 | if (!tokens.verify(process.env["PUBLIC_CSRF_TOKEN_SECRET"] as string, csrfToken)) {
53 | const csrfTokenSecret = process.env["PUBLIC_CSRF_TOKEN_SECRET"] as string;
54 | const csrfToken:any = tokens.create(csrfTokenSecret);
55 |
56 | res.cookie(cookiesSettings.COOKIE_CSRF_TOKEN_NAME, csrfToken, {
57 | httpOnly: true,
58 | secure: true,
59 | sameSite: 'none',
60 | path: '/',
61 | expires: new Date(new Date().getTime() + cookiesSettings.COOKIE_PUBLIC_CSRF_TOKEN_EXPIRATION)
62 | });
63 | }
64 |
65 | return res.status(401).json({message: 'Invalid Credential.', errorCode: errorCodes.NO_JWT_TOKEN_AUTHENTICATE_JWT_TOKEN});
66 | }
67 |
68 | jwt.verify(authenticationToken, process.env["AUTHENTICATION_TOKEN_SECRET"] as string, async (error: any, authenticatedUser: any): Promise => {
69 | if (error) {
70 | // THE USER HAS JWT TOKEN BUT INVALID
71 | res.cookie(cookiesSettings.COOKIE_AUTHENTICATION_TOKEN_NAME, 'expiredtoken', {
72 | httpOnly: true,
73 | secure: true,
74 | sameSite: 'none',
75 | path: '/',
76 | expires: new Date(0)
77 | });
78 |
79 | const csrfTokenSecret = process.env["PUBLIC_CSRF_TOKEN_SECRET"] as string;
80 | const csrfToken:any = tokens.create(csrfTokenSecret);
81 |
82 | res.cookie(cookiesSettings.COOKIE_CSRF_TOKEN_NAME, csrfToken, {
83 | httpOnly: true,
84 | secure: true,
85 | sameSite: 'none',
86 | path: '/',
87 | expires: new Date(new Date().getTime() + cookiesSettings.COOKIE_PUBLIC_CSRF_TOKEN_EXPIRATION)
88 | });
89 |
90 | return res.status(403).json({message: 'Invalid Credential.', errorCode: errorCodes.INVALID_JWT_TOKEN_AUTHENTICATE_JWT_TOKEN});
91 | }
92 |
93 | let existingUser = await User.findOne({_id: authenticatedUser._id})
94 | .select('-username -email -isSSO -createdAt -updatedAt')
95 | .populate('profile', '-fullName -profilePicture -createdAt -updatedAt')
96 | .populate('csrfTokenSecret')
97 | .populate('googleAuthenticator', '-isActivated -createdAt -updatedAt');
98 | if (!existingUser) return res.status(404).json({message: "Invalid Credential.", errorCode: errorCodes.NO_USER_FOUND_IN_DATABASE_INSIDE_JWT_DECODED_TOKEN_AUTHENTICATE_JWT_TOKEN});
99 | if (!tokens.verify(existingUser.csrfTokenSecret.secret, csrfToken)) {
100 | // THE USER HAS CSRF TOKEN BUT INVALID
101 | res.cookie(cookiesSettings.COOKIE_AUTHENTICATION_TOKEN_NAME, 'expiredtoken', {
102 | httpOnly: true,
103 | secure: true,
104 | sameSite: 'none',
105 | path: '/',
106 | expires: new Date(0)
107 | });
108 |
109 | const csrfTokenSecret = process.env["PUBLIC_CSRF_TOKEN_SECRET"] as string;
110 | const csrfToken:any = tokens.create(csrfTokenSecret);
111 |
112 | res.cookie(cookiesSettings.COOKIE_CSRF_TOKEN_NAME, csrfToken, {
113 | httpOnly: true,
114 | secure: true,
115 | sameSite: 'none',
116 | path: '/',
117 | expires: new Date(new Date().getTime() + cookiesSettings.COOKIE_PUBLIC_CSRF_TOKEN_EXPIRATION)
118 | });
119 |
120 | return res.status(403).json({message: 'Invalid Credential.', errorCode: errorCodes.INVALID_CSRF_TOKEN_VERIFY_PRIVATE_CSRF_TOKEN});
121 | }
122 |
123 | // * WE NO LONGER NEED CSRF TOKEN SECRET - THE USER IS REALLY AUTHENTICED USER
124 | existingUser.csrfTokenSecret = undefined;
125 |
126 | // * EXISTING USER - FINAL DATA - WE DON'T SEND THIS, WE HOLD THIS DATA FOR UPDATE PURPOSES
127 | // * {
128 | // * _id: ObjectId,
129 | // * profile: {
130 | // * _id: ObjectId
131 | // * },
132 | // * googleAuthenticator?: {
133 | // * _id: ObjectId,
134 | // * }
135 | // * }
136 |
137 | lodash.merge(req, { authenticatedUser: existingUser });
138 | next();
139 | });
140 | }
141 |
142 | export function verifyPublicCSRFToken(req: express.Request, res: express.Response, next: express.NextFunction): any {
143 | const csrfToken:any = req.cookies[cookiesSettings.COOKIE_CSRF_TOKEN_NAME];
144 | const tokens = new Tokens();
145 |
146 | if (csrfToken == null) {
147 | // THE USER HAS NO CSRF TOKEN
148 | const csrfTokenSecret = process.env["PUBLIC_CSRF_TOKEN_SECRET"] as string;
149 | const csrfToken:any = tokens.create(csrfTokenSecret);
150 |
151 | res.cookie(cookiesSettings.COOKIE_CSRF_TOKEN_NAME, csrfToken, {
152 | httpOnly: true,
153 | secure: true,
154 | sameSite: 'none',
155 | path: '/',
156 | expires: new Date(new Date().getTime() + cookiesSettings.COOKIE_PUBLIC_CSRF_TOKEN_EXPIRATION)
157 | });
158 |
159 | return res.status(401).json({message: 'Invalid Credential.', errorCode: errorCodes.NO_CSRF_TOKEN_VERIFY_PUBLIC_CSRF_TOKEN});
160 | }
161 |
162 | if (!tokens.verify(process.env["PUBLIC_CSRF_TOKEN_SECRET"] as string, csrfToken)) {
163 | // THE USER HAS CSRF TOKEN BUT INVALID
164 | const csrfTokenSecret = process.env["PUBLIC_CSRF_TOKEN_SECRET"] as string;
165 | const csrfToken:any = tokens.create(csrfTokenSecret);
166 |
167 | res.cookie(cookiesSettings.COOKIE_CSRF_TOKEN_NAME, csrfToken, {
168 | httpOnly: true,
169 | secure: true,
170 | sameSite: 'none',
171 | path: '/',
172 | expires: new Date(new Date().getTime() + cookiesSettings.COOKIE_PUBLIC_CSRF_TOKEN_EXPIRATION)
173 | });
174 |
175 | return res.status(403).json({message: 'Invalid Credential.', errorCode: errorCodes.INVALID_CSRF_TOKEN_VERIFY_PUBLIC_CSRF_TOKEN});
176 | }
177 |
178 | next();
179 | }
180 |
181 | export function sendPublicCSRFTokenToUser(req: express.Request, res: express.Response, next: express.NextFunction): any {
182 | // IF USER DOESN'T HAVE CSRF TOKEN, THE USER WILL RECEIVE A PUBLIC CSRF TOKEN
183 | const existingCsrfToken = req.cookies[cookiesSettings.COOKIE_CSRF_TOKEN_NAME];
184 |
185 | if (existingCsrfToken == null) {
186 | const tokens = new Tokens();
187 | const csrfTokenSecret = process.env["PUBLIC_CSRF_TOKEN_SECRET"] as string;
188 | const csrfToken = tokens.create(csrfTokenSecret);
189 |
190 | res.cookie(cookiesSettings.COOKIE_CSRF_TOKEN_NAME, csrfToken, {
191 | httpOnly: true,
192 | secure: true,
193 | sameSite: 'none',
194 | path: '/',
195 | expires: new Date(new Date().getTime() + cookiesSettings.COOKIE_PUBLIC_CSRF_TOKEN_EXPIRATION)
196 | });
197 | }
198 |
199 | next();
200 | }
201 |
--------------------------------------------------------------------------------
/frontend/src/assets/login-background.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/pages/MFA/LoginVerificationCode/LoginVerificationCode.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useNavigate, useOutletContext } from 'react-router-dom';
3 | import { Formik, Form, ErrorMessage } from 'formik';
4 | import { escape } from 'he';
5 | import * as Yup from 'yup';
6 | import DOMPurify from 'dompurify'; // FOR SANITIZING USER INPUT TO PREVENT XSS ATTACKS BEFORE SENDING TO THE BACKEND
7 | import axios from 'axios';
8 | import style from './LoginVerificationCode.module.css';
9 | import logo from '../../../assets/logo-header.png';
10 |
11 | import { useSelector, useDispatch } from 'react-redux';
12 | import { setDisable, setNotDisable, hasError, hasNoError } from '../../../actions';
13 | import { AllReducers } from '../../../interfaces';
14 |
15 | import CustomButton from '../../../components/AllRoutes/CustomButton/CustomButton';
16 | import CustomAlert from '../../../components/AllRoutes/CustomAlert/CustomAlert';
17 | import CustomInput from '../../../components/AllRoutes/CustomInput/CustomInput';
18 | import Layout from '../../../components/AllRoutes/Layout/Layout';
19 |
20 | type valuesSubmitType = {
21 | verificationCodeLogin: string
22 | }
23 |
24 | type valuesGoogleSubmitType = {
25 | googleAuthenticatorCodeLogin: string
26 | }
27 |
28 | const LoginVerificationCode = () => {
29 | const navigate = useNavigate();
30 | const [showButtonDisplayGoogleAuthenticatorForm, setShowButtonDisplayGoogleAuthenticatorForm] = useState(false);
31 | const [useGoogleAuthenticatorForm, setUseGoogleAuthenticatorForm] = useState(false);
32 | const [authenticatedUser]:any = useOutletContext();
33 | const error = useSelector((state: AllReducers) => state.error);
34 | const dispatch = useDispatch();
35 |
36 | const initialValues = {
37 | verificationCodeLogin: ''
38 | };
39 |
40 | const initialValuesGoogleAuthenticator = {
41 | googleAuthenticatorCodeLogin: ''
42 | };
43 |
44 | const validationSchema = Yup.object().shape({
45 | verificationCodeLogin: Yup.string()
46 | .required('Verification login code is required')
47 | .min(7, 'Verification login code must be 7 characters')
48 | .max(7, 'Verification login code must be 7 characters')
49 | .matches(/^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]{7}$/, 'Verification login code must be 7 characters and contain only numbers and letters')
50 | .test(
51 | 'verificationCodeLogin',
52 | 'Verification login code should not contain sensitive information',
53 | value => {
54 | return !/\b(admin|root|superuser)\b/i.test(value);
55 | }
56 | )
57 | .test(
58 | 'verificationCodeLogin',
59 | 'Invalid verification login code format or potentially unsafe characters',
60 | value => {
61 | const sanitizedValue = escape(value);
62 | return sanitizedValue === value;
63 | }
64 | )
65 | });
66 |
67 | const validationSchemaGoogleAuthenticator = Yup.object().shape({
68 | googleAuthenticatorCodeLogin: Yup.string()
69 | .required('Google Authenticator Code Login is required')
70 | .matches(/^\d{6}$/, 'Code must be a 6-digit number'),
71 | });
72 |
73 | const handleSubmit = (values: valuesSubmitType) => {
74 | const {verificationCodeLogin} = values;
75 | const sanitizedVerificationCodeLogin = DOMPurify.sanitize(verificationCodeLogin);
76 | dispatch(setDisable());
77 | axios.post(`${process.env.REACT_APP_API}/api/v1/authentication/verification-code-login`, {
78 | verificationCodeLogin: sanitizedVerificationCodeLogin
79 | })
80 | .then((response) => {
81 | if(response.status === 200 && response.data.status === 'ok') {
82 | dispatch(setNotDisable());
83 | if(error.hasError) {
84 | dispatch(hasNoError());
85 | }
86 | navigate('/home');
87 | }
88 | })
89 | .catch(function (error) {
90 | dispatch(setNotDisable());
91 | dispatch(hasError(error.response.data.message));
92 | });
93 | };
94 |
95 | const handleSubmitGoogleAuthenticator = (values: valuesGoogleSubmitType) => {
96 | const {googleAuthenticatorCodeLogin} = values;
97 | const sanitizedGoogleAuthenticatorCodeLogin = DOMPurify.sanitize(googleAuthenticatorCodeLogin);
98 | dispatch(setDisable());
99 | axios.post(`${process.env.REACT_APP_API}/api/v1/authentication/google-authenticator-code-login`, {
100 | googleAuthenticatorCodeLogin: sanitizedGoogleAuthenticatorCodeLogin
101 | })
102 | .then((response) => {
103 | if(response.status === 200 && response.data.status === 'ok') {
104 | dispatch(setNotDisable());
105 | if(error.hasError) {
106 | dispatch(hasNoError());
107 | }
108 | navigate('/home');
109 | }
110 | })
111 | .catch(function (error) {
112 | dispatch(setNotDisable());
113 | dispatch(hasError(error.response.data.message));
114 | });
115 | }
116 |
117 | const handleLogout = () => {
118 | dispatch(setDisable());
119 | axios.post(`${process.env.REACT_APP_API}/api/v1/authentication/verification-code-login/logout`)
120 | .then((response) => {
121 | if(response.status === 200 && response.data.status === 'ok') {
122 | dispatch(setNotDisable());
123 | if(error.hasError) {
124 | dispatch(hasNoError());
125 | }
126 | navigate('/login');
127 | }
128 | })
129 | .catch(function (error) {
130 | dispatch(setNotDisable());
131 | dispatch(hasError(error.response.data.message));
132 | });
133 | }
134 |
135 | const switchFormToGoogleAuthenticatorForm = () => {
136 | setUseGoogleAuthenticatorForm(true);
137 | }
138 |
139 | const switchSendVerificationCodeForm = () => {
140 | setUseGoogleAuthenticatorForm(false);
141 | }
142 |
143 | useEffect(() => {
144 | if(authenticatedUser.hasGoogleAuthenticator) {
145 | setShowButtonDisplayGoogleAuthenticatorForm(true);
146 | }
147 | // eslint-disable-next-line react-hooks/exhaustive-deps
148 | }, [])
149 |
150 | return (
151 | <>
152 | { !useGoogleAuthenticatorForm &&
153 | <>
154 |
155 |
164 |
165 |
166 |
167 |
188 |
189 |
190 | >
191 | }
192 |
193 | { useGoogleAuthenticatorForm &&
194 | <>
195 |
196 |
205 |
206 |
207 |
225 |
226 |
227 | >
228 | }
229 | >
230 | )
231 | }
232 |
233 | export default LoginVerificationCode;
--------------------------------------------------------------------------------