- Project Setup
- Backend Setup
- Frontend Setup
- Authentication System
- Role-Based Access Control
- OTP Verification
- Forget Password
- QR Code Login
- Google Authentication
- JWT Implementation
- Token Blacklist
- Testing and Deployment
Let's begin:
- 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
- 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}`));
- 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
- 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;
- 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;
- 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,
});
};
- 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,