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
2 changes: 1 addition & 1 deletion server/modules/auth/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,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
62 changes: 35 additions & 27 deletions server/modules/auth/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,29 @@ class AuthService {
// ── Registration ────────────────────────────────────────────────────────────

static async register({ name, email, password }) {
// Uniform response for every branch so registration can't be used to
// enumerate which emails are already taken.
const GENERIC_MESSAGE =
"If this email is available, a verification OTP has been sent. Please check your inbox.";
const existingUser = await AuthRepository.findUserByEmailWithoutPassword(email);

if (existingUser) {
throw new ApiError(409, "User already exists with this email");
// A verified account gets nothing (they should log in); an unverified
// one gets a fresh signup OTP so a legitimate re-registration still
// works. The response is identical either way — no account existence
// is revealed.
if (!existingUser.isVerified) {
await AuthRepository.deleteOtp(email, "signup");
const plainOtp = generateOTP();
const hashedOtp = await bcrypt.hash(plainOtp, 4);
await AuthRepository.createOtp({ email, otp: hashedOtp, purpose: "signup" });
await sendVerificationOTP(email, plainOtp);
}
return { message: GENERIC_MESSAGE };
}

const hashedPassword = await bcrypt.hash(password, 10);

const user = await AuthRepository.createUser({
await AuthRepository.createUser({
name,
email,
password: hashedPassword,
Expand All @@ -38,10 +53,7 @@ class AuthService {
await AuthRepository.createOtp({ email, otp: hashedOtp, purpose: "signup" });
await sendVerificationOTP(email, plainOtp);

return {
message: "Registration successful. Please check your email for OTP verification.",
user: { id: user._id, name: user.name, email: user.email, isVerified: user.isVerified }
};
return { message: GENERIC_MESSAGE };
}

// ── OTP Verification ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -111,21 +123,15 @@ class AuthService {
// ── Forgot / Reset Password ──────────────────────────────────────────────────

static async forgotPassword({ email }) {
const GENERIC_MESSAGE =
"If an account with that email exists, a password reset OTP has been sent.";
const user = await AuthRepository.findUserByEmailWithoutPassword(email);
if (!user) {
throw new ApiError(404, "User not found");
}

// Forgot password is only meaningful for users who have completed
// email verification. An unverified user should complete their signup
// first — they have an OTP flow for that, not a password reset flow.
if (!user.isVerified) {
throw new ApiError(403, "Please verify your email before resetting your password. Check your inbox for the original verification code or request a new one.");
}

// GitHub-only accounts have no password to reset.
if (user.authProvider === "github") {
throw new ApiError(400, "This account uses GitHub login. Password reset is not available.");
// Only a verified, password-based account can actually reset. Every other
// case (no user / unverified / GitHub-only) returns the same generic
// response so the endpoint never reveals whether an email is registered.
if (!user || !user.isVerified || user.authProvider === "github") {
return { message: GENERIC_MESSAGE };
}

const plainOtp = generateOTP();
Expand All @@ -134,7 +140,7 @@ class AuthService {
await AuthRepository.createOtp({ email, otp: hashedOtp, purpose: "forgot-password" });
await sendPasswordResetOTP(email, plainOtp);

return { message: "Password reset OTP sent to your email" };
return { message: GENERIC_MESSAGE };
}


Expand All @@ -159,13 +165,15 @@ class AuthService {
// ── Resend OTP ───────────────────────────────────────────────────────────────

static async resendOtp({ email, purpose }) {
const GENERIC_MESSAGE =
"If an account with that email exists, an OTP has been sent.";
const user = await AuthRepository.findUserByEmailWithoutPassword(email);
if (!user) {
throw new ApiError(404, "User not found");
}

if (purpose === "signup" && user.isVerified) {
throw new ApiError(400, "Already verified");
// No user, or a signup resend for an already-verified account: return the
// same generic response instead of a distinct error, so the endpoint
// can't be used to probe which emails are registered / verified.
if (!user || (purpose === "signup" && user.isVerified)) {
return { message: GENERIC_MESSAGE };
}

await AuthRepository.deleteOtp(email, purpose);
Expand All @@ -181,7 +189,7 @@ class AuthService {
await sendPasswordResetOTP(email, plainOtp);
}

return { message: "OTP resent successfully" };
return { message: GENERIC_MESSAGE };
}

// ── GitHub OAuth — Start ─────────────────────────────────────────────────────
Expand Down
Loading