-
Notifications
You must be signed in to change notification settings - Fork 143
Enable email verification on registration (Issue #717) #2265
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -114,8 +114,10 @@ const registerUser = async (req, res) => { | |
| // Generate default unique PrepPilot ID | ||
| const defaultPrepPilotId = cleanEmail.split("@")[0] + Math.floor(1000 + Math.random() * 9000); | ||
|
|
||
| // Email verification is currently disabled — accounts are active immediately on creation. | ||
| // To re-enable: set isEmailVerified to false, generate a token, and call sendVerificationEmail. | ||
| // Generate email verification token (24-hour expiry) | ||
| const rawVerificationToken = crypto.randomBytes(32).toString("hex"); | ||
| const hashedVerificationToken = crypto.createHash("sha256").update(rawVerificationToken).digest("hex"); | ||
|
|
||
| const user = await User.create({ | ||
| name: cleanName, | ||
| email: cleanEmail, | ||
|
|
@@ -133,25 +135,27 @@ const registerUser = async (req, res) => { | |
| socials: { github: "", linkedin: "", twitter: "", portfolio: "" } | ||
| }, | ||
| platformPreferences: { theme: "light", notificationsEnabled: true }, | ||
| isEmailVerified: true, // verification disabled — users can log in immediately | ||
| emailVerificationToken: null, | ||
| emailVerificationExpires: null, | ||
| isEmailVerified: false, | ||
| emailVerificationToken: hashedVerificationToken, | ||
| emailVerificationExpires: new Date(Date.now() + 24 * 60 * 60 * 1000), | ||
| }); | ||
|
|
||
| // Issue tokens immediately so the user is logged in right after signup | ||
| const accessToken = generateAccessToken(user._id, user.tokenVersion); | ||
| const refreshToken = generateRefreshToken(user._id); | ||
|
|
||
| user.refreshTokenHash = await bcrypt.hash(refreshToken, REFRESH_TOKEN_SALT_ROUNDS); | ||
| user.refreshTokenExpiresAt = new Date(Date.now() + REFRESH_TOKEN_MAX_AGE_MS); | ||
| await user.save(); | ||
|
|
||
| res.cookie("refreshToken", refreshToken, getRefreshCookieOptions()); | ||
| // Send verification email | ||
| const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${rawVerificationToken}`; | ||
| try { | ||
| await sendVerificationEmail(user.email, verificationUrl); | ||
| } catch (emailError) { | ||
| console.error("Failed to send verification email:", emailError); | ||
| await User.findByIdAndDelete(user._id); | ||
| return res.status(500).json({ | ||
| success: false, | ||
| message: "Failed to send verification email. Please try registering again.", | ||
| }); | ||
| } | ||
|
|
||
| return res.status(201).json({ | ||
| success: true, | ||
| message: "Account created successfully.", | ||
| accessToken, | ||
| message: "Account created successfully. Please verify your email to log in.", | ||
| _id: user._id, | ||
| name: user.name, | ||
| email: user.email, | ||
|
|
@@ -180,6 +184,14 @@ const loginUser = async (req, res) => { | |
| return res.status(401).json({ success: false, message: "Invalid email or password provided." }); | ||
| } | ||
|
|
||
| // Check if email is verified | ||
| if (!user.isEmailVerified) { | ||
| return res.status(403).json({ | ||
| success: false, | ||
| message: "Please verify your email before logging in. Check your inbox for a verification link.", | ||
| }); | ||
| } | ||
|
Comment on lines
+187
to
+193
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Validate the password before disclosing verification state. Line 188 returns 403 for an existing unverified account before Line 196 validates the password. An attacker can distinguish an unverified registered email from a nonexistent email or a verified account by submitting any password. Validate Proposed fix- // Check if email is verified
- if (!user.isEmailVerified) {
- return res.status(403).json({
- success: false,
- message: "Please verify your email before logging in. Check your inbox for a verification link.",
- });
- }
-
// Verify password against stored hash
const isMatch = await user.isValidPassword(password);
if (!isMatch) {
return res.status(401).json({ success: false, message: "Invalid email or password provided." });
}
+
+ if (!user.isEmailVerified) {
+ return res.status(403).json({
+ success: false,
+ message: "Please verify your email before logging in. Check your inbox for a verification link.",
+ });
+ }🤖 Prompt for AI Agents |
||
|
|
||
| // Verify password against stored hash | ||
| const isMatch = await user.isValidPassword(password); | ||
| if (!isMatch) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Do not delete the account after an ambiguous email-send failure.
sendVerificationEmailusesPromise.racewith a timeout. A timeout rejects this request but does not canceltransporter.sendMail. The SMTP operation can still send the verification email after Line 149 deletes the user. The recipient then receives a verification link that cannot succeed.Keep the unverified account and token when delivery status is uncertain. Return a response that directs the user to use the resend endpoint. Use an outbox if registration must guarantee asynchronous delivery.
🤖 Prompt for AI Agents