Skip to content

Commit c7abf06

Browse files
committed
[Feat] Add password reset migration and related SQL script for users table
1 parent 1bd3557 commit c7abf06

9 files changed

Lines changed: 758 additions & 1 deletion

File tree

Backend/package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Backend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@qdrant/js-client-rest": "^1.14.0",
2525
"bcryptjs": "^2.4.3",
2626
"cors": "^2.8.5",
27+
"crypto": "^1.0.1",
2728
"date-fns": "^3.6.0",
2829
"dotenv": "^16.5.0",
2930
"express": "^4.21.2",
@@ -33,6 +34,7 @@
3334
"mammoth": "^1.9.0",
3435
"multer": "^1.4.5-lts.2",
3536
"mysql2": "^3.14.0",
37+
"nodemailer": "^7.0.3",
3638
"openai": "^3.3.0",
3739
"pdf-parse": "^1.1.1",
3840
"sequelize": "^6.37.7",
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
const mysql = require('mysql2/promise');
2+
const fs = require('fs');
3+
const path = require('path');
4+
require('dotenv').config();
5+
6+
async function runPasswordResetMigration() {
7+
let connection;
8+
9+
try {
10+
console.log('[LOG password_reset_migration] ========= Starting password reset fields migration');
11+
12+
// Create connection
13+
connection = await mysql.createConnection({
14+
host: process.env.DB_HOST || 'localhost',
15+
user: process.env.DB_USER || 'root',
16+
password: process.env.DB_PASSWORD || 'root',
17+
database: process.env.DB_NAME || 'unihub_db',
18+
multipleStatements: true
19+
});
20+
21+
console.log('[LOG password_reset_migration] ========= Connected to database');
22+
23+
// Check if users table exists
24+
const [tables] = await connection.execute("SHOW TABLES LIKE 'users'");
25+
if (tables.length === 0) {
26+
throw new Error('Users table does not exist. Please run basic migrations first.');
27+
}
28+
29+
// Check if reset fields already exist
30+
const [columns] = await connection.execute("SHOW COLUMNS FROM users LIKE 'reset_password_token'");
31+
if (columns.length > 0) {
32+
console.log('[LOG password_reset_migration] ========= Password reset fields already exist, skipping migration');
33+
return;
34+
}
35+
36+
// Read and execute migration SQL
37+
const migrationSQL = fs.readFileSync(
38+
path.join(__dirname, 'src/migrations/add_password_reset_fields.sql'),
39+
'utf8'
40+
);
41+
42+
await connection.query(migrationSQL);
43+
console.log('[LOG password_reset_migration] ========= Password reset fields added successfully');
44+
45+
// Verify fields were added
46+
const [newColumns] = await connection.execute("SHOW COLUMNS FROM users WHERE Field IN ('reset_password_token', 'reset_password_expires')");
47+
console.log('[LOG password_reset_migration] ========= Added fields:', newColumns.map(col => col.Field));
48+
49+
} catch (error) {
50+
console.error('[LOG password_reset_migration] ========= Migration failed:', error.message);
51+
throw error;
52+
} finally {
53+
if (connection) {
54+
await connection.end();
55+
}
56+
}
57+
}
58+
59+
// Run if called directly
60+
if (require.main === module) {
61+
runPasswordResetMigration()
62+
.then(() => {
63+
console.log('[LOG password_reset_migration] ========= Migration completed successfully!');
64+
process.exit(0);
65+
})
66+
.catch((error) => {
67+
console.error('[LOG password_reset_migration] ========= Migration failed:', error);
68+
process.exit(1);
69+
});
70+
}
71+
72+
module.exports = { runPasswordResetMigration };

Backend/src/controllers/auth.controller.js

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
const bcrypt = require('bcryptjs');
22
const jwt = require('jsonwebtoken');
3+
const crypto = require('crypto');
34
const { v4: uuidv4 } = require('uuid');
45
const sequelize = require('../config/database');
56
const { QueryTypes } = require('sequelize');
7+
const emailService = require('../services/emailService');
68

79
const signup = async (req, res) => {
810
const { username, email, password, fullName } = req.body;
@@ -159,7 +161,180 @@ const login = async (req, res) => {
159161
}
160162
};
161163

164+
const forgotPassword = async (req, res) => {
165+
const { email } = req.body;
166+
167+
try {
168+
console.log(`[LOG forgot_password] ========= Password reset request for email: ${email}`);
169+
170+
// Find user by email
171+
const [user] = await sequelize.query(
172+
'SELECT * FROM users WHERE email = ?',
173+
{
174+
replacements: [email],
175+
type: QueryTypes.SELECT,
176+
}
177+
);
178+
179+
if (!user) {
180+
// Don't reveal if email exists or not for security
181+
return res.json({
182+
success: true,
183+
message: 'If an account with that email exists, we have sent a password reset link.'
184+
});
185+
}
186+
187+
// Generate reset token
188+
const resetToken = crypto.randomBytes(32).toString('hex');
189+
const resetTokenExpiry = new Date(Date.now() + 3600000); // 1 hour from now
190+
191+
// Save reset token to database
192+
await sequelize.query(
193+
'UPDATE users SET reset_password_token = ?, reset_password_expires = ? WHERE id = ?',
194+
{
195+
replacements: [resetToken, resetTokenExpiry, user.id],
196+
type: QueryTypes.UPDATE
197+
}
198+
);
199+
200+
// Send reset email
201+
try {
202+
await emailService.sendPasswordResetEmail(email, resetToken, user.username || user.full_name || 'User');
203+
console.log(`[LOG forgot_password] ========= Password reset email sent to ${email}`);
204+
} catch (emailError) {
205+
console.error(`[LOG forgot_password] ========= Failed to send email to ${email}:`, emailError);
206+
// Clear the reset token if email fails
207+
await sequelize.query(
208+
'UPDATE users SET reset_password_token = NULL, reset_password_expires = NULL WHERE id = ?',
209+
{
210+
replacements: [user.id],
211+
type: QueryTypes.UPDATE
212+
}
213+
);
214+
return res.status(500).json({
215+
success: false,
216+
message: 'Failed to send password reset email. Please try again later.'
217+
});
218+
}
219+
220+
res.json({
221+
success: true,
222+
message: 'If an account with that email exists, we have sent a password reset link.'
223+
});
224+
225+
} catch (error) {
226+
console.error('[LOG forgot_password] ========= Error in forgot password:', error);
227+
res.status(500).json({
228+
success: false,
229+
message: 'An error occurred while processing your request. Please try again later.'
230+
});
231+
}
232+
};
233+
234+
const resetPassword = async (req, res) => {
235+
const { token, newPassword } = req.body;
236+
237+
try {
238+
console.log(`[LOG reset_password] ========= Password reset attempt with token: ${token?.substring(0, 8)}...`);
239+
240+
// Find user by reset token and check if token is not expired
241+
const [user] = await sequelize.query(
242+
'SELECT * FROM users WHERE reset_password_token = ? AND reset_password_expires > NOW()',
243+
{
244+
replacements: [token],
245+
type: QueryTypes.SELECT,
246+
}
247+
);
248+
249+
if (!user) {
250+
return res.status(400).json({
251+
success: false,
252+
message: 'Invalid or expired reset token. Please request a new password reset.'
253+
});
254+
}
255+
256+
// Hash new password
257+
const salt = await bcrypt.genSalt(10);
258+
const hashedPassword = await bcrypt.hash(newPassword, salt);
259+
260+
// Update password and clear reset token
261+
await sequelize.query(
262+
'UPDATE users SET password_hash = ?, reset_password_token = NULL, reset_password_expires = NULL WHERE id = ?',
263+
{
264+
replacements: [hashedPassword, user.id],
265+
type: QueryTypes.UPDATE
266+
}
267+
);
268+
269+
// Send confirmation email
270+
try {
271+
await emailService.sendPasswordChangeConfirmation(user.email, user.username || user.full_name || 'User');
272+
console.log(`[LOG reset_password] ========= Password change confirmation sent to ${user.email}`);
273+
} catch (emailError) {
274+
console.error(`[LOG reset_password] ========= Failed to send confirmation email:`, emailError);
275+
// Don't fail the request if confirmation email fails
276+
}
277+
278+
console.log(`[LOG reset_password] ========= Password successfully reset for user ${user.username}`);
279+
280+
res.json({
281+
success: true,
282+
message: 'Password has been reset successfully. You can now log in with your new password.'
283+
});
284+
285+
} catch (error) {
286+
console.error('[LOG reset_password] ========= Error in reset password:', error);
287+
res.status(500).json({
288+
success: false,
289+
message: 'An error occurred while resetting your password. Please try again later.'
290+
});
291+
}
292+
};
293+
294+
const validateResetToken = async (req, res) => {
295+
const { token } = req.params;
296+
297+
try {
298+
console.log(`[LOG validate_token] ========= Validating reset token: ${token?.substring(0, 8)}...`);
299+
300+
// Check if token exists and is not expired
301+
const [user] = await sequelize.query(
302+
'SELECT id, email, username FROM users WHERE reset_password_token = ? AND reset_password_expires > NOW()',
303+
{
304+
replacements: [token],
305+
type: QueryTypes.SELECT,
306+
}
307+
);
308+
309+
if (!user) {
310+
return res.status(400).json({
311+
success: false,
312+
message: 'Invalid or expired reset token.'
313+
});
314+
}
315+
316+
res.json({
317+
success: true,
318+
message: 'Token is valid.',
319+
data: {
320+
email: user.email,
321+
username: user.username
322+
}
323+
});
324+
325+
} catch (error) {
326+
console.error('[LOG validate_token] ========= Error validating token:', error);
327+
res.status(500).json({
328+
success: false,
329+
message: 'An error occurred while validating the token.'
330+
});
331+
}
332+
};
333+
162334
module.exports = {
163335
signup,
164-
login
336+
login,
337+
forgotPassword,
338+
resetPassword,
339+
validateResetToken
165340
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Add password reset fields to users table
2+
ALTER TABLE users
3+
ADD COLUMN reset_password_token VARCHAR(255) NULL,
4+
ADD COLUMN reset_password_expires DATETIME NULL;
5+
6+
-- Add index for faster lookups
7+
CREATE INDEX idx_users_reset_token ON users(reset_password_token);

0 commit comments

Comments
 (0)