Skip to content

Commit 82d1b65

Browse files
committed
feat: functional magic links
1 parent aa501df commit 82d1b65

2 files changed

Lines changed: 20 additions & 22 deletions

File tree

src/controllers/magicLinks.ts

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -70,40 +70,27 @@ export async function requestMagicLink(req: Request, res: Response) {
7070
}
7171

7272
export async function verifyMagicLink(req: Request, res: Response) {
73-
const parse = MagicLinkVerifyQuerySchema.safeParse(req.query);
73+
const { token } = req.params;
7474

75-
if (!parse.success) {
76-
return res.redirect('/login?error=invalid');
75+
if (!token) {
76+
return res.status(400).json({ message: 'Missing verification token' });
7777
}
78-
79-
const { token } = parse.data;
8078
const tokenHash = hashSha256(token);
8179

8280
const record = await MagicLinkToken.findOne({
8381
where: { token_hash: tokenHash },
8482
});
8583

8684
if (!record) {
87-
return res.redirect('/login?error=invalid');
85+
return res.status(400).json({ message: 'Invalid verification token' });
8886
}
8987

9088
if (record.used_at) {
91-
return res.redirect('/login?error=used');
89+
return res.status(400).json({ message: 'Invalid verification token' });
9290
}
9391

9492
if (record.expires_at < new Date()) {
95-
return res.redirect('/login?error=expired');
96-
}
97-
98-
// Device binding check
99-
const { ip_hash, user_agent_hash } = hashDeviceFingerprint(req.ip, req.headers['user-agent']);
100-
101-
if (record.ip_hash && record.ip_hash !== ip_hash) {
102-
return res.redirect('/login?error=device_mismatch');
103-
}
104-
105-
if (record.user_agent_hash && record.user_agent_hash !== user_agent_hash) {
106-
return res.redirect('/login?error=device_mismatch');
93+
return res.status(400).json({ message: 'Invalid verification token' });
10794
}
10895

10996
// Atomic consume
@@ -118,7 +105,7 @@ export async function verifyMagicLink(req: Request, res: Response) {
118105
);
119106

120107
if (!updated) {
121-
return res.redirect('/login?error=invalid');
108+
return res.status(500).json({ message: 'Failed to use token' });
122109
}
123110

124111
await AuthEventService.log({
@@ -127,7 +114,18 @@ export async function verifyMagicLink(req: Request, res: Response) {
127114
req,
128115
});
129116

130-
return res.redirect(record.redirect_url || '/');
117+
// Device binding check
118+
const { ip_hash, user_agent_hash } = hashDeviceFingerprint(req.ip, req.headers['user-agent']);
119+
120+
if (record.ip_hash && record.ip_hash !== ip_hash) {
121+
return res.status(200).json({ message: 'Success' });
122+
}
123+
124+
if (record.user_agent_hash && record.user_agent_hash !== user_agent_hash) {
125+
return res.status(200).json({ message: 'Success' });
126+
}
127+
128+
return res.status(200).json({ message: 'Success' });
131129
}
132130

133131
export async function pollMagicLinkConfirmation(req: Request, res: Response) {

src/routes/magicLink.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ router.get(
2323
);
2424

2525
router.get('/poll', attachAuthMiddleware('ephemeral'), pollMagicLinkConfirmation);
26-
router.get('/verify/:token', attachAuthMiddleware('ephemeral'), verifyMagicLink);
26+
router.get('/verify/:token', verifyMagicLink);
2727

2828
export default router;

0 commit comments

Comments
 (0)