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
97 changes: 77 additions & 20 deletions controller/loginController.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ const crypto = require("crypto");
const supabase = require("../dbConnection");
const { validationResult } = require("express-validator");

// ✅ Set SendGrid API key once globally
sgMail.setApiKey(process.env.SENDGRID_KEY);
// ✅ Initialize SendGrid API key once globally (support multiple env var names)
const _sendgridKey = process.env.SENDGRID_API_KEY || process.env.SENDGRID_KEY;
if (_sendgridKey) {
sgMail.setApiKey(_sendgridKey);
} else {
console.warn("SendGrid API key not set (SENDGRID_API_KEY or SENDGRID_KEY). Email sending will be disabled.");
}

const login = async (req, res) => {
const errors = validationResult(req);
Expand Down Expand Up @@ -92,10 +97,40 @@ const login = async (req, res) => {
if (user.mfa_enabled) {
const token = crypto.randomInt(100000, 999999);
await addMfaToken(user.user_id, token);
await sendOtpEmail(user.email, token);
return res.status(202).json({
message: "An MFA Token has been sent to your email address"
});
// If developer explicitly requests the OTP be sent to client (for local testing), skip calling SendGrid
const exposeOtpToClient = (process.env.NODE_ENV !== 'production') && (process.env.SEND_OTP_TO_CLIENT === 'true');
console.log('DEBUG: NODE_ENV=', process.env.NODE_ENV, 'SEND_OTP_TO_CLIENT=', process.env.SEND_OTP_TO_CLIENT, 'exposeOtpToClient=', exposeOtpToClient);

if (exposeOtpToClient) {
// Skip sending via SendGrid to avoid 'Maximum credits exceeded' during local dev
// Display the MFA token prominently in terminal for developer testing
console.log('');
console.log('🔐 ==============================================');
console.log('📧 [DEV] MFA Token Generated for Testing:');
console.log(`📱 Email: ${user.email}`);
console.log(`🔢 MFA Code: ${token}`);
console.log('🔐 ==============================================');
console.log('');

// Also expose the token in a response header so frontends can read it automatically
res.setHeader('X-DEV-MFA-TOKEN', token);
// Allow browsers to access this custom header
res.setHeader('Access-Control-Expose-Headers', 'X-DEV-MFA-TOKEN');
const responseBody = { message: "An MFA Token has been requested for your account", token };
return res.status(202).json(responseBody);
}

// production/default: attempt to send via SendGrid as before
const sendResult = await sendOtpEmail(user.email, token);

if (!sendResult?.ok) {
console.warn('sendOtpEmail failed', sendResult);
const responseBody = { message: "An MFA Token has been requested for your account" };
if (process.env.NODE_ENV !== 'production') responseBody.sendgrid = sendResult;
return res.status(202).json(responseBody);
}

return res.status(202).json({ message: "An MFA Token has been sent to your email address" });
}

await logLoginEvent({
Expand All @@ -106,13 +141,19 @@ const login = async (req, res) => {
});

// ✅ RBAC-aware JWT generation
const jwtSecret = process.env.JWT_SECRET || process.env.JWT_TOKEN || process.env.JWT;
if (!jwtSecret) {
console.error('JWT secret is not configured. Set JWT_SECRET (or JWT_TOKEN) in your environment.');
return res.status(500).json({ error: 'Server configuration error: missing JWT secret' });
}

const token = jwt.sign(
{
userId: user.user_id,
role: user.user_roles?.role_name || "unknown"
},
process.env.JWT_TOKEN,
{ expiresIn: "1h" }
jwtSecret,
{ expiresIn: "10m" } // 修改为10分钟
);

return res.status(200).json({ user, token });
Expand Down Expand Up @@ -154,13 +195,19 @@ const loginMfa = async (req, res) => {
}

// ✅ RBAC-aware JWT
const jwtSecret = process.env.JWT_SECRET || process.env.JWT_TOKEN || process.env.JWT;
if (!jwtSecret) {
console.error('JWT secret is not configured. Set JWT_SECRET (or JWT_TOKEN) in your environment.');
return res.status(500).json({ error: 'Server configuration error: missing JWT secret' });
}

const token = jwt.sign(
{
userId: user.user_id,
role: user.user_roles?.role_name || "unknown"
},
process.env.JWT_TOKEN,
{ expiresIn: "1h" }
jwtSecret,
{ expiresIn: "10m" } // 修改为10分钟
);

return res.status(200).json({ user, token });
Expand All @@ -174,33 +221,43 @@ const loginMfa = async (req, res) => {
// ✅ Send OTP email via SendGrid
async function sendOtpEmail(email, token) {
try {
const from = process.env.FROM_EMAIL || process.env.SENDGRID_FROM || 'noreply@nutrihelp.com';
if (!_sendgridKey) {
console.warn(`Not sending OTP email to ${email} because SendGrid is not configured. OTP token: ${token}`);
return { ok: false, reason: 'sendgrid_not_configured', token };
}

await sgMail.send({
to: email,
from: process.env.SENDGRID_FROM,
from,
subject: "NutriHelp Login Token",
text: `Your token to log in is ${token}`,
html: `Your token to log in is <strong>${token}</strong>`
});
console.log("OTP email sent successfully to", email);
return { ok: true };
} catch (err) {
console.error("Error sending OTP email:", err.response?.body || err.message);
const errBody = err.response?.body || err.message;
console.error("Error sending OTP email:", errBody);
// If SendGrid returns an error like 'Maximum credits exceeded', surface that to caller
return { ok: false, reason: 'sendgrid_error', detail: errBody };
}
}

// ✅ Send failed login alert via SendGrid
async function sendFailedLoginAlert(email, ip) {
try {
const from = process.env.FROM_EMAIL || process.env.SENDGRID_FROM || 'noreply@nutrihelp.com';
if (!_sendgridKey) {
console.warn(`Not sending failed-login alert to ${email} because SendGrid is not configured. IP: ${ip}`);
return;
}

await sgMail.send({
from: process.env.SENDGRID_FROM,
from,
to: email,
subject: "Failed Login Attempt on NutriHelp",
text: `Hi,

Someone tried to log in to NutriHelp using your email address from IP: ${ip}.

If this wasn't you, please ignore this message. But if you're concerned, consider resetting your password or contacting support.

– NutriHelp Security Team`
text: `Hi,\n\nSomeone tried to log in to NutriHelp using your email address from IP: ${ip}.\n\nIf this wasn't you, please ignore this message. But if you're concerned, consider resetting your password or contacting support.\n\n– NutriHelp Security Team`
});
console.log(`Failed login alert sent to ${email}`);
} catch (err) {
Expand Down
1 change: 1 addition & 0 deletions controller/signupController.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { registerValidation } = require('../validators/signupValidator.js');
// const supabase = require('../dbConnection');
const logLoginEvent = require("../Monitor_&_Logging/loginLogger");
const { supabase } = require("../database/supabase");
const { createClient } = require('@supabase/supabase-js'); // Add missing import

const safeLog = async (payload) => {
try { await logLoginEvent(payload); } catch (e) { console.warn("log error:", e.message); }
Expand Down
13 changes: 8 additions & 5 deletions services/authService.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const supabase = createClient(

class AuthService {
constructor() {
this.accessTokenExpiry = '15m'; // 15 minutes
this.accessTokenExpiry = '10m'; // 10 minutes - 修改为10分钟
this.refreshTokenExpiry = 7 * 24 * 60 * 60 * 1000; // 7 days
}

Expand Down Expand Up @@ -47,9 +47,12 @@ class AuthService {
last_name,
role_id: 7,
account_status: 'active',
email_verified: false,
mfa_enabled: false,
registration_date: new Date().toISOString()
//email_verified: false,
//mfa_enabled: false,
//registration_date: new Date().toISOString()
mfa_enabled: true, // ✅ Enable MFA by default
// Remove fields that don't exist in database schema
// first_name, last_name, role_id, account_status, email_verified, registration_date
})
.select('user_id, email, name')
.single();
Expand Down Expand Up @@ -181,7 +184,7 @@ class AuthService {
return {
accessToken,
refreshToken,
expiresIn: 15 * 60, // 15 minutes in seconds
expiresIn: 10 * 60, // 10 minutes in seconds - 修改为10分钟
tokenType: 'Bearer'
};

Expand Down
6 changes: 4 additions & 2 deletions validators/signupValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ const registerValidation = [
.matches(/[^A-Za-z0-9]/).withMessage('Password needs a special character'),

body('contact_number')
.notEmpty().withMessage('Contact number is required')
//.notEmpty().withMessage('Contact number is required')
.optional() // Make optional for /api/auth/register compatibility
.isMobilePhone().withMessage('Please enter a valid contact number'),

body('address')
.notEmpty().withMessage('Address is required')
//.notEmpty().withMessage('Address is required')
.optional() // Make optional for /api/auth/register compatibility
.isLength({ min: 10 }).withMessage('Address should be at least 10 characters long'),
];

Expand Down
Loading