Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
services:
mongo:
image: mongo
restart: always
volumes:
- mongo_data:/data/db
env_file: ./server/.env
redis:
image: redis
restart: always
backend:
build:
context: ./server
dockerfile: Dockerfile.dev
depends_on:
- mongo
- redis
ports:
- "8000:8000"
volumes:
- /app/node_modules
- ./server:/app
env_file:
- ./server/.env
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With docker-compose, REDIS_URL=redis://localhost:6379 from .env will point to the backend container itself, not the redis service. Document or set REDIS_URL to redis://redis:6379 for the compose environment to work out-of-the-box.

Suggested change
- ./server/.env
- ./server/.env
environment:
REDIS_URL: redis://redis:6379

Copilot uses AI. Check for mistakes.
volumes:
mongo_data:
10 changes: 0 additions & 10 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions server/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM node:lts-alpine

WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY ./ ./

CMD ["npm", "run", "server"]
28 changes: 28 additions & 0 deletions server/REDIS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Feature: Redis-Based OTP Verification & Auth Flow Refactoring 🚀

## 📝 Description
This PR refactors the authentication and registration flow to utilize **Redis** for temporary data storage (OTPs and pending user sessions) instead of MongoDB. This architectural change significantly improves database hygiene by preventing the database from being cluttered with unverified or abandoned accounts.

## ✨ Key Changes & Architectural Improvements

1. **Optimized User Creation (Database Hygiene)**
- Removed the `isVerified` field from the `User` model.
- **Old Flow:** Users were created in MongoDB immediately upon registration with `isVerified: false`.
- **New Flow:** User data is now temporarily cached in Redis under the key `pending_user:<email>` with a 10-minute expiration. The user is ONLY saved to MongoDB permanently *after* they successfully verify their OTP.

2. **Redis-Powered OTP Management**
- Replaced the MongoDB `Otp.js` model with Redis key-value storage (`otp:<purpose>:<email>`).
- Leverages Redis's native TTL (Time-To-Live) capabilities to automatically expire OTPs and pending users after 10 minutes, reducing database load and removing the need for MongoDB TTL indexes.
- Safely deleted the obsolete `server/models/Otp.js` file.

3. **Refactored Auth Modules**
- Updated `AuthRepository` to include Redis cache interactions (`storePendingUser`, `getPendingUser`, `storeOtp`, etc.).
- Refactored `AuthService` and `AuthController` to seamlessly handle registration, forgot password, reset password, and resend OTP flows using the new Redis architecture.

## 🛠 Prerequisites for Reviewers/Maintainers
Because of the new Redis dependency, the environment setup has been updated. To run this locally:

1. Ensure you have a Redis server running locally or via Docker.
2. Add the following to your `server/.env` file:
```env
REDIS_URL=redis://localhost:6379 # Or your respective Redis connection string
15 changes: 15 additions & 0 deletions server/config/redis.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createClient } from 'redis';

export const redisClient = createClient({ url: process.env.REDIS_URL });

redisClient.on('error', (err) => console.error('Redis Client Error:', err));

export const connectToRedis = async () => {
try {
await redisClient.connect();
console.log("Connected to Redis cache.");
} catch (error) {
console.error("Failed to connect to Redis:", error);
Comment thread
agentVivek marked this conversation as resolved.
throw error;
}
};
10 changes: 0 additions & 10 deletions server/models/Otp.js

This file was deleted.

1 change: 0 additions & 1 deletion server/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true, lowercase: true, index: true },
password: { type: String, required: true, minlength: 6, select: false },
role: { type: String, enum: ["user", "admin"], default: "user" },
isVerified: { type: Boolean, default: false },
authProvider: { type: String, enum: ["local", "google", "github"], default: "local" },

// Profile Info
Expand Down
2 changes: 1 addition & 1 deletion server/modules/auth/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class AuthController {
static async register(req, res, next) {
try {
const result = await AuthService.register(req.body);
res.status(201).json(ApiResponse.success(result.message, result.user));
res.status(201).json(ApiResponse.success(result.message));
} catch (error) {
next(error instanceof ApiError ? error : new ApiError(500, error.message));
}
Expand Down
49 changes: 23 additions & 26 deletions server/modules/auth/repository.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import User from "../../models/User.js";
import Otp from "../../models/Otp.js";
import {redisClient} from "../../config/redis.js";

class AuthRepository {
static async createUser(userData) {
const user = new User(userData);
return await user.save();
}

static async updateUserPassword(email, hashedPassword) {
return await User.findOneAndUpdate(
{ email },
{ password: hashedPassword },
{ new: true }
);
}
static async findUserByEmail(email) {
return await User.findOne({ email }).select("+password");
}
Expand All @@ -15,41 +21,32 @@ class AuthRepository {
return await User.findOne({ email });
}

static async findUserById(id) {
return await User.findById(id);
// --- Redis OTP Methods ---
static async storePendingUser(email, data, ttl = 600) {
// Stores user data and OTP in Redis with a 10-minute expiry
await redisClient.set(`pending_user:${email}`, JSON.stringify(data), "EX", ttl);
}
Comment on lines +25 to +28
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redisClient.set() in node-redis v5 expects an options object for TTL; passing "EX", ttl as positional args is not supported and can result in missing expiry or runtime errors. Use the supported options form so the pending user key actually expires.

Copilot uses AI. Check for mistakes.

static async updateUserVerification(email) {
return await User.findOneAndUpdate(
{ email },
{ isVerified: true },
{ new: true }
);
static async getPendingUser(email) {
const data = await redisClient.get(`pending_user:${email}`);
return data ? JSON.parse(data) : null;
}

static async updateUserPassword(email, hashedPassword) {
return await User.findOneAndUpdate(
{ email },
{ password: hashedPassword },
{ new: true }
);
static async deletePendingUser(email) {
await redisClient.del(`pending_user:${email}`);
}

static async createOtp({ email, otp, purpose }) {
// Delete any existing OTPs for same email+purpose
await Otp.deleteMany({ email, purpose });

const otpRecord = new Otp({ email, otp, purpose });
return await otpRecord.save();
static async storeOtp(email, purpose, otp, ttl = 600) {
await redisClient.set(`otp:${purpose}:${email}`, otp, "EX", ttl);
}
Comment on lines +38 to 40
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same TTL issue here: redisClient.set() should be called with an options object for expiration, otherwise OTP keys may not expire as intended.

Copilot uses AI. Check for mistakes.

static async findOtp(email, purpose) {
return await Otp.findOne({ email, purpose }).sort({ createdAt: -1 });
static async getOtp(email, purpose) {
return await redisClient.get(`otp:${purpose}:${email}`);
}

static async deleteOtp(email, purpose) {
return await Otp.deleteMany({ email, purpose });
await redisClient.del(`otp:${purpose}:${email}`);
}
}

export default AuthRepository;

Loading