Skip to content

Commit a362d77

Browse files
committed
fix(auth): add magic link API endpoint
1 parent 86d82ba commit a362d77

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

app/api/auth/magic-link/route.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import 'server-only'
2+
import { createClient } from '@supabase/supabase-js'
3+
import { NextRequest, NextResponse } from 'next/server'
4+
import { loginRateLimit } from '@/lib/utils/rate-limiter'
5+
6+
function isValidEmail(email: string) {
7+
const atIndex = email.lastIndexOf('@')
8+
9+
return (
10+
atIndex > 0 &&
11+
atIndex < email.length - 3 &&
12+
!email.includes(' ') &&
13+
email.slice(atIndex + 1).includes('.')
14+
)
15+
}
16+
17+
function getRequestOrigin(request: NextRequest) {
18+
const forwardedHost = request.headers.get('x-forwarded-host')
19+
const forwardedProto = request.headers.get('x-forwarded-proto')
20+
21+
if (forwardedHost) {
22+
return `${forwardedProto || 'https'}://${forwardedHost}`
23+
}
24+
25+
return new URL(request.url).origin
26+
}
27+
28+
export async function POST(request: NextRequest) {
29+
try {
30+
const rateLimitResult = await loginRateLimit(request)
31+
32+
if (!rateLimitResult.allowed) {
33+
return NextResponse.json(
34+
{
35+
error: 'Too many magic link requests. Please try again later.',
36+
lockedUntil: rateLimitResult.lockedUntil,
37+
resetTime: rateLimitResult.resetTime,
38+
},
39+
{ status: 429 }
40+
)
41+
}
42+
43+
const body = await request.json()
44+
const email = typeof body?.email === 'string' ? body.email.trim().toLowerCase() : ''
45+
46+
if (!email) {
47+
return NextResponse.json({ error: 'Email is required' }, { status: 400 })
48+
}
49+
50+
if (!isValidEmail(email)) {
51+
return NextResponse.json({ error: 'Please enter a valid email address' }, { status: 400 })
52+
}
53+
54+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
55+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
56+
57+
if (!supabaseUrl || !supabaseAnonKey) {
58+
console.error('Missing Supabase public configuration for magic link auth')
59+
return NextResponse.json(
60+
{ error: 'Authentication is not configured. Please contact support.' },
61+
{ status: 500 }
62+
)
63+
}
64+
65+
const supabase = createClient(supabaseUrl, supabaseAnonKey, {
66+
auth: {
67+
autoRefreshToken: false,
68+
persistSession: false,
69+
},
70+
})
71+
72+
const origin = getRequestOrigin(request)
73+
const { error } = await supabase.auth.signInWithOtp({
74+
email,
75+
options: {
76+
emailRedirectTo: `${origin}/auth/callback`,
77+
},
78+
})
79+
80+
if (error) {
81+
console.error('Magic link auth error:', error.message)
82+
return NextResponse.json({ error: error.message }, { status: 400 })
83+
}
84+
85+
return NextResponse.json({
86+
success: true,
87+
message: 'Check your email for the magic link.',
88+
})
89+
} catch (error) {
90+
console.error('Magic link route error:', error)
91+
return NextResponse.json(
92+
{ error: 'Unable to send magic link. Please try again.' },
93+
{ status: 500 }
94+
)
95+
}
96+
}

0 commit comments

Comments
 (0)