projects
Role Baseed Authentication
  1. Project Setup
  2. Backend Setup
  3. Frontend Setup
  4. Authentication System
  5. Role-Based Access Control
  6. OTP Verification
  7. Forget Password
  8. QR Code Login
  9. Google Authentication
  10. JWT Implementation
  11. Token Blacklist
  12. Testing and Deployment

Let's begin:

  1. Project Setup

First, let's set up our project structure:

project-root/
├── backend/
│   ├── src/
│   │   ├── config/
│   │   ├── controllers/
│   │   ├── middleware/
│   │   ├── models/
│   │   ├── routes/
│   │   ├── services/
│   │   └── utils/
│   ├── .env
│   ├── package.json
│   └── tsconfig.json
└── frontend/
    ├── src/
    │   ├── components/
    │   ├── pages/
    │   ├── styles/
    │   ├── utils/
    │   └── types/
    ├── .env.local
    ├── next.config.js
    ├── package.json
    └── tsconfig.json
  1. Backend Setup

a. Initialize the backend project:

mkdir backend
cd backend
npm init -y
npm install express mongoose dotenv bcryptjs jsonwebtoken cors nodemailer qrcode speakeasy @types/express @types/mongoose @types/bcryptjs @types/jsonwebtoken @types/cors @types/nodemailer @types/qrcode @types/speakeasy
npm install --save-dev typescript ts-node nodemon @types/node

b. Create a tsconfig.json file:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

c. Update package.json scripts:

"scripts": {
  "start": "node dist/server.js",
  "dev": "nodemon src/server.ts",
  "build": "tsc"
}

d. Create a .env file:

MONGODB_URI=your_mongodb_connection_string
JWT_SECRET=your_jwt_secret
JWT_REFRESH_SECRET=your_jwt_refresh_secret
SMTP_HOST=your_smtp_host
SMTP_PORT=your_smtp_port
SMTP_USER=your_smtp_user
SMTP_PASS=your_smtp_password
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret

e. Create src/server.ts:

import express from 'express';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import cors from 'cors';
import authRoutes from './routes/authRoutes';
import userRoutes from './routes/userRoutes';
 
dotenv.config();
 
const app = express();
 
app.use(cors());
app.use(express.json());
 
mongoose.connect(process.env.MONGODB_URI as string)
  .then(() => console.log('Connected to MongoDB'))
  .catch((err) => console.error('MongoDB connection error:', err));
 
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
 
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
  1. Frontend Setup

a. Create a new Next.js project with TypeScript:

npx create-next-app@latest frontend --typescript
cd frontend
npm install next-auth@beta @auth/mongodb-adapter mongodb

b. Update next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  env: {
    NEXTAUTH_URL: process.env.NEXTAUTH_URL,
    NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
  },
}
 
module.exports = nextConfig

c. Create .env.local:

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your_nextauth_secret
  1. Authentication System

a. Create backend/src/models/User.ts:

import mongoose, { Schema, Document } from 'mongoose';
 
export interface IUser extends Document {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  password: string;
  role: 'user' | 'manager' | 'admin';
  googleId?: string;
  otpSecret?: string;
  isVerified: boolean;
}
 
const UserSchema: Schema = new Schema({
  firstName: { type: String, required: true },
  lastName: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  phone: { type: String, required: true },
  password: { type: String, required: true },
  role: { type: String, enum: ['user', 'manager', 'admin'], default: 'user' },
  googleId: { type: String },
  otpSecret: { type: String },
  isVerified: { type: Boolean, default: false },
});
 
export default mongoose.model<IUser>('User', UserSchema);

b. Create backend/src/controllers/authController.ts:

import { Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import User from '../models/User';
import { sendVerificationEmail } from '../utils/emailService';
import { generateOTP, verifyOTP } from '../utils/otpService';
import { generateQRCode } from '../utils/qrCodeService';
 
export const register = async (req: Request, res: Response) => {
  try {
    const { firstName, lastName, email, phone, password, role } = req.body;
 
    const existingUser = await User.findOne({ email });
    if (existingUser) {
      return res.status(400).json({ message: 'User already exists' });
    }
 
    const hashedPassword = await bcrypt.hash(password, 12);
    const otpSecret = generateOTP();
 
    const newUser = new User({
      firstName,
      lastName,
      email,
      phone,
      password: hashedPassword,
      role,
      otpSecret,
    });
 
    await newUser.save();
 
    await sendVerificationEmail(email, otpSecret);
 
    res.status(201).json({ message: 'User registered successfully' });
  } catch (error) {
    res.status(500).json({ message: 'Something went wrong' });
  }
};
 
export const login = async (req: Request, res: Response) => {
  try {
    const { email, password } = req.body;
 
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(400).json({ message: 'Invalid credentials' });
    }
 
    const isPasswordCorrect = await bcrypt.compare(password, user.password);
    if (!isPasswordCorrect) {
      return res.status(400).json({ message: 'Invalid credentials' });
    }
 
    if (!user.isVerified) {
      return res.status(400).json({ message: 'Please verify your email first' });
    }
 
    const token = jwt.sign(
      { userId: user._id, email: user.email, role: user.role },
      process.env.JWT_SECRET as string,
      { expiresIn: '1h' }
    );
 
    const refreshToken = jwt.sign(
      { userId: user._id },
      process.env.JWT_REFRESH_SECRET as string,
      { expiresIn: '7d' }
    );
 
    res.status(200).json({ token, refreshToken, user: { id: user._id, email: user.email, role: user.role } });
  } catch (error) {
    res.status(500).json({ message: 'Something went wrong' });
  }
};
 
export const verifyEmail = async (req: Request, res: Response) => {
  try {
    const { email, otp } = req.body;
 
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(400).json({ message: 'User not found' });
    }
 
    const isOTPValid = verifyOTP(user.otpSecret, otp);
    if (!isOTPValid) {
      return res.status(400).json({ message: 'Invalid OTP' });
    }
 
    user.isVerified = true;
    await user.save();
 
    res.status(200).json({ message: 'Email verified successfully' });
  } catch (error) {
    res.status(500).json({ message: 'Something went wrong' });
  }
};
 
export const forgotPassword = async (req: Request, res: Response) => {
  try {
    const { email } = req.body;
 
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(400).json({ message: 'User not found' });
    }
 
    const resetToken = jwt.sign(
      { userId: user._id },
      process.env.JWT_SECRET as string,
      { expiresIn: '1h' }
    );
 
    // Send reset password email with resetToken
    // Implement email sending logic here
 
    res.status(200).json({ message: 'Reset password email sent' });
  } catch (error) {
    res.status(500).json({ message: 'Something went wrong' });
  }
};
 
export const resetPassword = async (req: Request, res: Response) => {
  try {
    const { resetToken, newPassword } = req.body;
 
    const decodedToken = jwt.verify(resetToken, process.env.JWT_SECRET as string) as { userId: string };
    const user = await User.findById(decodedToken.userId);
 
    if (!user) {
      return res.status(400).json({ message: 'Invalid token' });
    }
 
    const hashedPassword = await bcrypt.hash(newPassword, 12);
    user.password = hashedPassword;
    await user.save();
 
    res.status(200).json({ message: 'Password reset successful' });
  } catch (error) {
    res.status(500).json({ message: 'Something went wrong' });
  }
};
 
export const generateQRCode = async (req: Request, res: Response) => {
  try {
    const { userId } = req.body;
 
    const user = await User.findById(userId);
    if (!user) {
      return res.status(400).json({ message: 'User not found' });
    }
 
    const qrCodeData = await generateQRCode(user.email);
 
    res.status(200).json({ qrCodeData });
  } catch (error) {
    res.status(500).json({ message: 'Something went wrong' });
  }
};
 
export const loginWithQRCode = async (req: Request, res: Response) => {
  try {
    const { qrCode } = req.body;
 
    // Implement QR code verification logic here
    // This is a placeholder implementation
    const email = qrCode; // In a real scenario, you'd decode the QR code to get the email
 
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(400).json({ message: 'Invalid QR code' });
    }
 
    const token = jwt.sign(
      { userId: user._id, email: user.email, role: user.role },
      process.env.JWT_SECRET as string,
      { expiresIn: '1h' }
    );
 
    const refreshToken = jwt.sign(
      { userId: user._id },
      process.env.JWT_REFRESH_SECRET as string,
      { expiresIn: '7d' }
    );
 
    res.status(200).json({ token, refreshToken, user: { id: user._id, email: user.email, role: user.role } });
  } catch (error) {
    res.status(500).json({ message: 'Something went wrong' });
  }
};

c. Create backend/src/routes/authRoutes.ts:

import express from 'express';
import { register, login, verifyEmail, forgotPassword, resetPassword, generateQRCode, loginWithQRCode } from '../controllers/authController';
 
const router = express.Router();
 
router.post('/register', register);
router.post('/login', login);
router.post('/verify-email', verifyEmail);
router.post('/forgot-password', forgotPassword);
router.post('/reset-password', resetPassword);
router.post('/generate-qr', generateQRCode);
router.post('/login-qr', loginWithQRCode);
 
export default router;
  1. Role-Based Access Control

a. Create backend/src/middleware/authMiddleware.ts:

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
 
interface AuthRequest extends Request {
  userId?: string;
  userRole?: string;
}
 
export const authMiddleware = (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const token = req.headers.authorization?.split(' ')[1];
    if (!token) {
      return res.status(401).json({ message: 'Authentication failed' });
    }
 
    const decodedToken = jwt.verify(token, process.env.JWT_SECRET as string) as { userId: string, role: string };
    req.userId = decodedToken.userId;
    req.userRole = decodedToken.role;
    next();
  } catch (error) {
    res.status(401).json({ message: 'Authentication failed' });
  }
};
 
export const roleMiddleware = (roles: string[]) => {
  return (req: AuthRequest, res: Response, next: NextFunction) => {
    if (!req.userRole || !roles.includes(req.userRole)) {
      return res.status(403).json({ message: 'Access denied' });
    }
    next();
  };
};

b. Update backend/src/routes/userRoutes.ts:

import express from 'express';
import { authMiddleware, roleMiddleware } from '../middleware/authMiddleware';
import { getUsers, getUserById, updateUser, deleteUser } from '../controllers/userController';
 
const router = express.Router();
 
router.get('/', authMiddleware, roleMiddleware(['admin']), getUsers);
router.get('/:id', authMiddleware, getUserById);
router.put('/:id', authMiddleware, updateUser);
router.delete('/:id', authMiddleware, roleMiddleware(['admin']), deleteUser);
 
export default router;
  1. OTP Verification

a. Create backend/src/utils/otpService.ts:

import speakeasy from 'speakeasy';
 
export const generateOTP = () => {
  const secret = speakeasy.generateSecret({ length: 32 });
  return secret.base32;
};
 
export const verifyOTP = (secret: string, token: string) => {
  return speakeasy.totp.verify({
    secret,
    encoding: 'base32',
    token,
  });
};
  1. Forget Password

The forgot password functionality is already implemented in the authController.ts file. You'll need to create an email service to send the reset password link.

a. Create backend/src/utils/emailService.ts:

import nodemailer from 'nodemailer';
 
const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,