Skip to content

Commit 6cbae7e

Browse files
small update in codebase
1 parent 4577080 commit 6cbae7e

5 files changed

Lines changed: 546 additions & 0 deletions

File tree

backend/src/controllers/AuthController.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,98 @@ export class AuthController {
175175
res.status(400).json({ error: error.message, requestId: req.requestId });
176176
}
177177
}
178+
179+
async requestMagicLink(req, res, next) {
180+
try {
181+
const { email } = req.body;
182+
if (!email) {
183+
return res.status(400).json({ error: 'Email is required', requestId: req.requestId });
184+
}
185+
186+
await this.userService.requestMagicLink(email);
187+
res.json({
188+
requestId: req.requestId,
189+
timestamp: new Date().toISOString(),
190+
message: 'If the email exists, a magic login link has been generated.'
191+
});
192+
} catch (error) {
193+
next(error);
194+
}
195+
}
196+
197+
async loginWithMagicLink(req, res, next) {
198+
try {
199+
const { token } = req.query;
200+
if (!token) {
201+
return res.status(400).json({ error: 'Token parameter is required', requestId: req.requestId });
202+
}
203+
204+
const clientIp = req.ip || req.headers['x-forwarded-for'] || req.socket.remoteAddress;
205+
const userAgent = req.headers['user-agent'] || 'Unknown';
206+
207+
const result = await this.userService.loginWithMagicLink(token, clientIp, userAgent);
208+
209+
res.cookie('token', result.token, {
210+
httpOnly: true,
211+
secure: process.env.NODE_ENV === 'production',
212+
sameSite: 'strict',
213+
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
214+
});
215+
216+
res.json({
217+
requestId: req.requestId,
218+
timestamp: new Date().toISOString(),
219+
message: 'Magic login successful',
220+
token: result.token,
221+
user: result.user
222+
});
223+
} catch (error) {
224+
res.status(401).json({ error: error.message, requestId: req.requestId });
225+
}
226+
}
227+
228+
async getSessions(req, res, next) {
229+
try {
230+
const userId = req.user.userId;
231+
const sessions = await this.userService.getActiveSessions(userId);
232+
res.json({
233+
requestId: req.requestId,
234+
timestamp: new Date().toISOString(),
235+
sessions
236+
});
237+
} catch (error) {
238+
next(error);
239+
}
240+
}
241+
242+
async getLoginHistory(req, res, next) {
243+
try {
244+
const userId = req.user.userId;
245+
const history = await this.userService.getLoginHistory(userId);
246+
res.json({
247+
requestId: req.requestId,
248+
timestamp: new Date().toISOString(),
249+
history
250+
});
251+
} catch (error) {
252+
next(error);
253+
}
254+
}
255+
256+
async revokeSession(req, res, next) {
257+
try {
258+
const userId = req.user.userId;
259+
const { tokenHash } = req.params;
260+
await this.userService.revokeSession(userId, tokenHash);
261+
res.json({
262+
requestId: req.requestId,
263+
timestamp: new Date().toISOString(),
264+
message: 'Session revoked successfully.'
265+
});
266+
} catch (error) {
267+
res.status(400).json({ error: error.message, requestId: req.requestId });
268+
}
269+
}
178270
}
179271

180272
export default AuthController;

backend/src/infrastructure/repositories/UserRepository.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,16 @@ export class UserRepository {
177177
}
178178
}
179179

180+
// Find user by magic link token
181+
async findByMagicToken(token) {
182+
try {
183+
return await User.findOne({ 'metadata.magicToken': token });
184+
} catch (error) {
185+
logger.error('Error finding user by magic token', { error: error.message });
186+
throw error;
187+
}
188+
}
189+
180190
// Find by ID and select specific fields
181191
async findByIdWithFields(id, fields = []) {
182192
try {

backend/src/routes/authRoutes.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ export const createAuthRoutes = (controller) => {
2626
router.post('/mfa/enable', requireAuth, (req, res, next) => controller.enableMfa(req, res, next));
2727
router.post('/mfa/disable', requireAuth, (req, res, next) => controller.disableMfa(req, res, next));
2828

29+
// Magic link login
30+
router.post('/magic-link', authRateLimiter, (req, res, next) => controller.requestMagicLink(req, res, next));
31+
router.get('/magic-link/callback', (req, res, next) => controller.loginWithMagicLink(req, res, next));
32+
33+
// Active Sessions & History (requires authentication)
34+
router.get('/sessions', requireAuth, (req, res, next) => controller.getSessions(req, res, next));
35+
router.get('/sessions/history', requireAuth, (req, res, next) => controller.getLoginHistory(req, res, next));
36+
router.delete('/sessions/:tokenHash', requireAuth, (req, res, next) => controller.revokeSession(req, res, next));
37+
2938
return router;
3039
};
3140

backend/src/services/UserService.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,109 @@ export class UserService {
381381
throw error;
382382
}
383383
}
384+
385+
// Generate magic link token and log it
386+
async requestMagicLink(email) {
387+
try {
388+
const user = await this.userRepository.findByEmail(email);
389+
if (!user) {
390+
throw new Error('User not found');
391+
}
392+
393+
const magicToken = crypto.randomBytes(32).toString('hex');
394+
// Store token in user metadata temporarily
395+
user.metadata = {
396+
...user.metadata,
397+
magicToken,
398+
magicTokenExpires: Date.now() + 900000 // 15 mins expiration
399+
};
400+
await user.save();
401+
402+
logger.info(`[MAILER MOCK] Magic Login Link for ${email}: http://localhost:5173/login?magic_token=${magicToken}`);
403+
return true;
404+
} catch (error) {
405+
logger.error('Error in requestMagicLink', { email, error: error.message });
406+
throw error;
407+
}
408+
}
409+
410+
// Login using magic link token
411+
async loginWithMagicLink(token, clientIp = '127.0.0.1', device = 'Unknown') {
412+
try {
413+
const user = await this.userRepository.findByMagicToken(token);
414+
if (!user || !user.metadata?.magicTokenExpires || user.metadata.magicTokenExpires < Date.now()) {
415+
throw new Error('Invalid or expired magic link token');
416+
}
417+
418+
// Clear magic token
419+
const meta = { ...user.metadata };
420+
delete meta.magicToken;
421+
delete meta.magicTokenExpires;
422+
user.metadata = meta;
423+
424+
// Log history
425+
user.loginHistory.push({
426+
ipAddress: clientIp,
427+
device,
428+
timestamp: new Date()
429+
});
430+
await user.save();
431+
432+
const userObj = user.toObject();
433+
delete userObj.passwordHash;
434+
435+
// Generate JWT
436+
const jwtToken = jwt.sign(
437+
{
438+
userId: userObj._id,
439+
email: userObj.email,
440+
subscriptionTier: userObj.subscriptionTier,
441+
},
442+
config.security.jwtSecret,
443+
{ expiresIn: config.security.jwtExpiresIn }
444+
);
445+
446+
// Save active session token hash
447+
const tokenHash = crypto.createHash('sha256').update(jwtToken).digest('hex');
448+
user.activeSessions.push({
449+
tokenHash,
450+
device,
451+
createdAt: new Date()
452+
});
453+
await user.save();
454+
455+
return { token: jwtToken, user: userObj };
456+
} catch (error) {
457+
logger.error('Error in loginWithMagicLink', { error: error.message });
458+
throw error;
459+
}
460+
}
461+
462+
// Get active sessions
463+
async getActiveSessions(userId) {
464+
const user = await this.getUser(userId);
465+
return user.activeSessions || [];
466+
}
467+
468+
// Get login history
469+
async getLoginHistory(userId) {
470+
const user = await this.getUser(userId);
471+
return user.loginHistory || [];
472+
}
473+
474+
// Revoke an active session token
475+
async revokeSession(userId, tokenHash) {
476+
try {
477+
const user = await this.getUser(userId);
478+
user.activeSessions = user.activeSessions.filter(s => s.tokenHash !== tokenHash);
479+
await user.save();
480+
logger.info('Session revoked successfully', { userId, tokenHash });
481+
return true;
482+
} catch (error) {
483+
logger.error('Error in revokeSession', { userId, tokenHash, error: error.message });
484+
throw error;
485+
}
486+
}
384487
}
385488

386489
export default UserService;

0 commit comments

Comments
 (0)