Skip to content
40 changes: 20 additions & 20 deletions server/config/env.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@

import dotenv from "dotenv";

dotenv.config();
// FORCE load .env from correct path
dotenv.config({ path: "./.env" });

const requiredEnvVars = [
"PORT",
"MONGO_URI",
"JWT_SECRET",
"JWT_EXPIRES_IN",
"GEMINI_API_KEY",
"CLIENT_URL",
"NODE_ENV",
"SMTP_HOST",
"SMTP_PORT",
"SMTP_USER",
"SMTP_PASS"
"SMTP_PASS",
"GEMINI_API_KEY",
"GITHUB_CLIENT_ID",
"GITHUB_CLIENT_SECRET",
"GITHUB_CALLBACK_URL"
];

const missingEnvVars = requiredEnvVars.filter(envVar => !process.env[envVar]);
const missingEnvVars = requiredEnvVars.filter(
(key) => !process.env[key]
);

console.log("DEBUG ENV:", {
PORT: process.env.PORT,
MONGO_URI: process.env.MONGO_URI ? "OK" : "MISSING"
});

if (missingEnvVars.length > 0) {
throw new Error(`Missing required environment variables: ${missingEnvVars.join(", ")}`);
throw new Error(
`Missing required environment variables: ${missingEnvVars.join(", ")}`
);
}

export const env = {
PORT: process.env.PORT,
MONGO_URI: process.env.MONGO_URI,
JWT_SECRET: process.env.JWT_SECRET,
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN,
GEMINI_API_KEY: process.env.GEMINI_API_KEY,
CLIENT_URL: process.env.CLIENT_URL,
NODE_ENV: process.env.NODE_ENV,
SMTP_HOST: process.env.SMTP_HOST,
SMTP_PORT: parseInt(process.env.SMTP_PORT, 10),
SMTP_USER: process.env.SMTP_USER,
SMTP_PASS: process.env.SMTP_PASS
};

export default env;
export default process.env;
15 changes: 15 additions & 0 deletions server/middlewares/rateLimiter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import rateLimit from "express-rate-limit";

export const otpLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes

max: 10,

message: {
success: false,
message: "Too many OTP requests. Please try again later.",
},

standardHeaders: true,
legacyHeaders: false,
});
46 changes: 42 additions & 4 deletions server/models/Otp.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@

import mongoose from "mongoose";

const otpSchema = new mongoose.Schema({
email: { type: String, required: true, lowercase: true, index: true },
otp: { type: String, required: true }, // stored hashed
purpose: { type: String, enum: ["signup", "forgot-password"], required: true },
createdAt: { type: Date, default: Date.now, expires: 600 } // TTL 10 minutes
email: {
type: String,
required: true,
lowercase: true,
index: true,
},

otp: {
type: String,
required: true,
},

purpose: {
type: String,
enum: ["signup", "forgot-password"],
required: true,
},

failedAttempts: {
type: Number,
default: 0,
},

lockUntil: {
type: Date,
default: null,
},

createdAt: {
type: Date,
default: Date.now,
expires: 1200,
},
Comment on lines +33 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep OTP expiry aligned with the email copy.

expires: 1200 keeps OTPs around for 20 minutes, but both OTP email templates still tell users the code expires in 10 minutes. If 20 minutes is intentional to cover the 15-minute lockout window, the templates need to be updated from the same source of truth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/models/Otp.js` around lines 33 - 37, The createdAt field in the Otp
mongoose schema uses expires: 1200 (20 minutes) but OTP email templates state 10
minutes; either set expires to 600 to match the templates or update the email
templates to reflect 20 minutes—change the value in the createdAt config in
server/models/Otp.js (the createdAt field's expires option) and ensure the same
canonical duration is used in the OTP email generation code/templates so both
code and copy share one source of truth.

});

otpSchema.index(
{ email: 1, purpose: 1 },
{ unique: true }
);

export default mongoose.model("Otp", otpSchema);



112 changes: 95 additions & 17 deletions server/modules/auth/repository.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@

import User from "../../models/User.js";
import Otp from "../../models/Otp.js";

class AuthRepository {
// =========================
// USER METHODS
// =========================

static async createUser(userData) {
const user = new User(userData);
return await user.save();
Expand All @@ -26,51 +31,124 @@ class AuthRepository {
static async updateUserVerification(email) {
return await User.findOneAndUpdate(
{ email },
{ isVerified: true },
{ new: true }
{
$set: {
isVerified: true,
},
},
{ new: true, runValidators: true }
);
}

static async updateUserPassword(email, hashedPassword) {
return await User.findOneAndUpdate(
{ email },
{ password: hashedPassword },
{ new: true }
{
$set: {
password: hashedPassword,
},
},
{ new: true, runValidators: true }
);
}

static async updateUserGithubIdentity(userId, githubIdentity = {}) {
const updateData = {
"oauth.github.id": githubIdentity.id,
"oauth.github.username": githubIdentity.username,
"oauth.github.profileUrl": githubIdentity.profileUrl,
"handles.github": githubIdentity.username,
"oauth.github.id": githubIdentity.id,
"oauth.github.username": githubIdentity.username,
"oauth.github.profileUrl": githubIdentity.profileUrl,
"handles.github": githubIdentity.username,
};

if (githubIdentity.avatarUrl) {
updateData["profile.avatar"] = githubIdentity.avatarUrl;
}

if (githubIdentity.accessToken) {
updateData["oauth.github.accessToken"] = githubIdentity.accessToken;
updateData["oauth.github.accessToken"] =
githubIdentity.accessToken;
}

return await User.findByIdAndUpdate(
userId,
updateData,
{ new: true, runValidators: true }
{
$set: updateData,
},
{
new: true,
runValidators: true,
}
);
}

// =========================
// OTP METHODS
// =========================

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();
return await Otp.findOneAndUpdate(
{ email, purpose },
{
$set: {
otp,
failedAttempts: 0,
lockUntil: null,
createdAt: new Date(),
},
},
{
upsert: true,
new: true,
setDefaultsOnInsert: true,
}
);
}

static async findOtp(email, purpose) {
return await Otp.findOne({ email, purpose }).sort({ createdAt: -1 });
return await Otp.findOne({ email, purpose });
}

// =========================
// OTP SAFETY METHODS
// =========================

static async incrementOtpFailure(
email,
purpose,
threshold = 5,
lockMinutes = 15
) {
const otpDoc = await Otp.findOne({ email, purpose });

if (!otpDoc) {
return null;
}

otpDoc.failedAttempts =
(otpDoc.failedAttempts || 0) + 1;

if (otpDoc.failedAttempts >= threshold) {
otpDoc.lockUntil = new Date(
Date.now() + lockMinutes * 60 * 1000
);
}

await otpDoc.save();

return otpDoc;
}

static async resetOtp(email, purpose) {
return await Otp.findOneAndUpdate(
{ email, purpose },
{
$set: {
failedAttempts: 0,
lockUntil: null,
},
},
{ new: true }
);
}

static async deleteOtp(email, purpose) {
Expand Down
Loading