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
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"
];
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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: 600,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

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

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



93 changes: 79 additions & 14 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 @@ -27,50 +32,109 @@ class AuthRepository {
return await User.findOneAndUpdate(
{ email },
{ isVerified: true },
{ new: true }
{ returnDocument: "after" }
);
}

static async updateUserPassword(email, hashedPassword) {
return await User.findOneAndUpdate(
{ email },
{ password: hashedPassword },
{ new: true }
{ returnDocument: "after" }
);
}

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 }
{
returnDocument: "after",
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,
}
);
}

// =========================
// OTP FETCH
// =========================

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) {
return await Otp.findOneAndUpdate(
{ email, purpose },
{ $inc: { failedAttempts: 1 } },
{ returnDocument: "after" }
);
}

static async lockOtp(email, purpose, lockMinutes = 15) {
return await Otp.findOneAndUpdate(
{ email, purpose },
{
lockUntil: new Date(
Date.now() + lockMinutes * 60 * 1000
),
},
{ returnDocument: "after" }
);
}

static async resetOtp(email, purpose) {
return await Otp.findOneAndUpdate(
{ email, purpose },
{
failedAttempts: 0,
lockUntil: null,
},
{ returnDocument: "after" }
);
}

static async deleteOtp(email, purpose) {
Expand All @@ -79,3 +143,4 @@ class AuthRepository {
}

export default AuthRepository;

88 changes: 79 additions & 9 deletions server/modules/auth/routes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Router } from "express";

import {
registerSchema,
loginSchema,
Expand All @@ -11,36 +12,105 @@ import {
validate,
validateQuery
} from "./validation.js";

import AuthController from "./controller.js";

import authMiddleware from "../../middlewares/authMiddleware.js";

import { otpLimiter } from "../../middlewares/rateLimiter.js";

import rateLimit from "express-rate-limit";

const router = Router();

// =========================
// GitHub Connect Rate Limiter
// =========================
const githubConnectRateLimit = rateLimit({
windowMs: 60 * 1000,
max: 20,

standardHeaders: true,
legacyHeaders: false,

message: {
success: false,
message: "Too many GitHub connect attempts. Please try again shortly."
}
});

router.post("/register", validate(registerSchema), AuthController.register);
router.post("/verify-otp", validate(verifyOtpSchema), AuthController.verifyOtp);
router.post("/login", validate(loginSchema), AuthController.login);
router.post("/forgot-password", validate(forgotPasswordSchema), AuthController.forgotPassword);
router.post("/reset-password", validate(resetPasswordSchema), AuthController.resetPassword);
router.post("/resend-otp", validate(resendOtpSchema), AuthController.resendOtp);
router.get("/github/start", validateQuery(githubStartSchema), AuthController.startGithubAuth);
// =========================
// AUTH ROUTES
// =========================

// Register
router.post(
"/register",
validate(registerSchema),
AuthController.register
);

// Verify OTP (Protected with rate limiter)
router.post(
"/verify-otp",
otpLimiter,
validate(verifyOtpSchema),
AuthController.verifyOtp
);

// Login
router.post(
"/login",
validate(loginSchema),
AuthController.login
);

// Forgot Password
router.post(
"/forgot-password",
validate(forgotPasswordSchema),
AuthController.forgotPassword
);

// Reset Password
router.post(
"/reset-password",
validate(resetPasswordSchema),
AuthController.resetPassword
);

// Resend OTP
router.post(
"/resend-otp",
validate(resendOtpSchema),
AuthController.resendOtp
);

// =========================
// GITHUB AUTH
// =========================

// Start GitHub OAuth
router.get(
"/github/start",
validateQuery(githubStartSchema),
AuthController.startGithubAuth
);

// Connect GitHub Account
router.get(
"/github/connect",
githubConnectRateLimit,
authMiddleware,
validateQuery(githubStartSchema),
AuthController.startGithubConnect
);
router.get("/github/callback", validateQuery(githubCallbackSchema), AuthController.githubCallback);

export default router;
// GitHub Callback
router.get(
"/github/callback",
validateQuery(githubCallbackSchema),
AuthController.githubCallback
);

export default router;
Loading