API Routes, Route Handlers, and Server Actions
Next.js is the primary reference implementation for Turalogin. The integration works naturally with API routes, route handlers, or server actions. Your backend calls Turalogin to start auth, the user clicks the magic link in their email, and your callback page verifies the token and sets its own session cookie. The frontend never sees a token.
Set up your environment variables for local development and production.
1 # Your Turalogin API key from the dashboard 2 TURALOGIN_API_KEY=tl_live_xxxxxxxxxxxxx 3 4 # The URL where magic links will redirect to 5 # Development: 6 APP_LOGIN_VALIDATION_URL=http://localhost:3000/auth/callback 7 8 # Production (uncomment and update): 9 # APP_LOGIN_VALIDATION_URL=https://myapp.com/auth/callback
Create an API route to initiate authentication. This sends a magic link to the user's email that redirects to your callback URL.
1 import { NextResponse } from 'next/server'; 2 3 const TURALOGIN_API_KEY = process.env.TURALOGIN_API_KEY!; 4 const TURALOGIN_API_URL = 'https://api.turalogin.com/api/v1'; 5 const VALIDATION_URL = process.env.APP_LOGIN_VALIDATION_URL!; 6 7 export async function POST(request: Request) { 8 try { 9 const { email } = await request.json(); 10 11 if (!email) { 12 return NextResponse.json( 13 { error: 'Email is required' }, 14 { status: 400 } 15 ); 16 } 17 18 const response = await fetch(`${TURALOGIN_API_URL}/auth/start`, { 19 method: 'POST', 20 headers: { 21 'Authorization': `Bearer ${TURALOGIN_API_KEY}`, 22 'Content-Type': 'application/json', 23 }, 24 body: JSON.stringify({ 25 email, 26 validationUrl: VALIDATION_URL // Where the magic link redirects to 27 }), 28 }); 29 30 if (!response.ok) { 31 const error = await response.json(); 32 return NextResponse.json(error, { status: response.status }); 33 } 34 35 const data = await response.json(); 36 return NextResponse.json({ 37 success: true, 38 message: 'Check your email for the login link' 39 }); 40 41 } catch (error) { 42 console.error('Auth start error:', error); 43 return NextResponse.json( 44 { error: 'Internal server error' }, 45 { status: 500 } 46 ); 47 } 48 }
Create a callback page that receives the token from the magic link and verifies it with Turalogin.
1 'use client'; 2 3 import { useEffect, useState } from 'react'; 4 import { useRouter, useSearchParams } from 'next/navigation'; 5 6 export default function AuthCallbackPage() { 7 const router = useRouter(); 8 const searchParams = useSearchParams(); 9 const [error, setError] = useState(''); 10 const [verifying, setVerifying] = useState(true); 11 12 useEffect(() => { 13 const token = searchParams.get('token'); 14 15 if (!token) { 16 setError('Invalid login link'); 17 setVerifying(false); 18 return; 19 } 20 21 // Verify the token with our API 22 fetch('/api/auth/verify', { 23 method: 'POST', 24 headers: { 'Content-Type': 'application/json' }, 25 body: JSON.stringify({ sessionId: token }), 26 }) 27 .then(async (res) => { 28 const data = await res.json(); 29 if (!res.ok) throw new Error(data.error); 30 31 // Success! Redirect to dashboard 32 router.push('/dashboard'); 33 }) 34 .catch((err) => { 35 setError(err.message || 'Verification failed'); 36 setVerifying(false); 37 }); 38 }, [searchParams, router]); 39 40 if (verifying) { 41 return ( 42 <div className="min-h-screen flex items-center justify-center"> 43 <div className="text-center"> 44 <div className="animate-spin w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full mx-auto mb-4" /> 45 <p>Verifying your login...</p> 46 </div> 47 </div> 48 ); 49 } 50 51 return ( 52 <div className="min-h-screen flex items-center justify-center"> 53 <div className="text-center"> 54 <p className="text-red-500 mb-4">{error}</p> 55 <a href="/login" className="text-blue-500 hover:underline"> 56 Try again 57 </a> 58 </div> 59 </div> 60 ); 61 }
Verify the token from the magic link and exchange it for a JWT, then create your session.
1 import { NextResponse } from 'next/server'; 2 import { cookies } from 'next/headers'; 3 4 const TURALOGIN_API_KEY = process.env.TURALOGIN_API_KEY!; 5 const TURALOGIN_API_URL = 'https://api.turalogin.com/api/v1'; 6 7 export async function POST(request: Request) { 8 try { 9 const { sessionId } = await request.json(); 10 11 if (!sessionId) { 12 return NextResponse.json( 13 { error: 'Token is required' }, 14 { status: 400 } 15 ); 16 } 17 18 // Verify the token with Turalogin 19 const response = await fetch(`${TURALOGIN_API_URL}/auth/verify`, { 20 method: 'POST', 21 headers: { 22 'Authorization': `Bearer ${TURALOGIN_API_KEY}`, 23 'Content-Type': 'application/json', 24 }, 25 body: JSON.stringify({ sessionId }), 26 }); 27 28 if (!response.ok) { 29 const error = await response.json(); 30 return NextResponse.json(error, { status: response.status }); 31 } 32 33 const { token, user } = await response.json(); 34 35 // Create your own session 36 const session = await createSession(user, token); 37 38 // Set session cookie 39 const cookieStore = await cookies(); 40 cookieStore.set('session', session.id, { 41 httpOnly: true, 42 secure: process.env.NODE_ENV === 'production', 43 sameSite: 'lax', 44 maxAge: 60 * 60 * 24 * 7, // 7 days 45 }); 46 47 return NextResponse.json({ success: true, user }); 48 49 } catch (error) { 50 console.error('Auth verify error:', error); 51 return NextResponse.json( 52 { error: 'Internal server error' }, 53 { status: 500 } 54 ); 55 } 56 } 57 58 async function createSession(user: any, token: string) { 59 // Implement your session storage (database, Redis, etc.) 60 // This is where you control your own session logic 61 return { id: crypto.randomUUID(), userId: user.id, token }; 62 }
If you prefer Server Actions, you can use them instead of API routes for a more integrated experience.
1 'use server'; 2 3 import { cookies } from 'next/headers'; 4 import { redirect } from 'next/navigation'; 5 6 const TURALOGIN_API_KEY = process.env.TURALOGIN_API_KEY!; 7 const TURALOGIN_API_URL = 'https://api.turalogin.com/api/v1'; 8 const VALIDATION_URL = process.env.APP_LOGIN_VALIDATION_URL!; 9 10 export async function startAuth(formData: FormData) { 11 const email = formData.get('email') as string; 12 13 const response = await fetch(`${TURALOGIN_API_URL}/auth/start`, { 14 method: 'POST', 15 headers: { 16 'Authorization': `Bearer ${TURALOGIN_API_KEY}`, 17 'Content-Type': 'application/json', 18 }, 19 body: JSON.stringify({ 20 email, 21 validationUrl: VALIDATION_URL 22 }), 23 }); 24 25 const data = await response.json(); 26 27 if (!response.ok) { 28 return { error: data.error || 'Failed to start authentication' }; 29 } 30 31 return { success: true, message: 'Check your email for the login link' }; 32 } 33 34 export async function verifyAuth(token: string) { 35 const response = await fetch(`${TURALOGIN_API_URL}/auth/verify`, { 36 method: 'POST', 37 headers: { 38 'Authorization': `Bearer ${TURALOGIN_API_KEY}`, 39 'Content-Type': 'application/json', 40 }, 41 body: JSON.stringify({ sessionId: token }), 42 }); 43 44 const data = await response.json(); 45 46 if (!response.ok) { 47 return { error: data.error || 'Invalid or expired link' }; 48 } 49 50 // Create session and set cookie 51 const cookieStore = await cookies(); 52 cookieStore.set('session', data.token, { 53 httpOnly: true, 54 secure: true, 55 sameSite: 'lax', 56 maxAge: 60 * 60 * 24 * 7, 57 }); 58 59 redirect('/dashboard'); 60 }
Protect routes using Next.js middleware to check for valid sessions.
1 import { NextResponse } from 'next/server'; 2 import type { NextRequest } from 'next/server'; 3 4 const protectedRoutes = ['/dashboard', '/settings', '/api/user']; 5 const authRoutes = ['/login', '/register']; 6 7 export function middleware(request: NextRequest) { 8 const session = request.cookies.get('session'); 9 const { pathname } = request.nextUrl; 10 11 // Check if accessing protected route without session 12 if (protectedRoutes.some(route => pathname.startsWith(route))) { 13 if (!session) { 14 const loginUrl = new URL('/login', request.url); 15 loginUrl.searchParams.set('redirect', pathname); 16 return NextResponse.redirect(loginUrl); 17 } 18 } 19 20 // Redirect authenticated users away from auth pages 21 if (authRoutes.some(route => pathname.startsWith(route))) { 22 if (session) { 23 return NextResponse.redirect(new URL('/dashboard', request.url)); 24 } 25 } 26 27 return NextResponse.next(); 28 } 29 30 export const config = { 31 matcher: [ 32 '/dashboard/:path*', 33 '/settings/:path*', 34 '/api/user/:path*', 35 '/login', 36 '/register', 37 ], 38 };
A complete login page component with email input and success message after sending the magic link.
1 'use client'; 2 3 import { useState } from 'react'; 4 5 export default function LoginPage() { 6 const [email, setEmail] = useState(''); 7 const [error, setError] = useState(''); 8 const [success, setSuccess] = useState(false); 9 const [loading, setLoading] = useState(false); 10 11 async function handleSubmit(e: React.FormEvent) { 12 e.preventDefault(); 13 setError(''); 14 setLoading(true); 15 16 try { 17 const response = await fetch('/api/auth/start', { 18 method: 'POST', 19 headers: { 'Content-Type': 'application/json' }, 20 body: JSON.stringify({ email }), 21 }); 22 23 const data = await response.json(); 24 25 if (!response.ok) { 26 throw new Error(data.error || 'Failed to send login link'); 27 } 28 29 setSuccess(true); 30 } catch (err) { 31 setError(err instanceof Error ? err.message : 'Something went wrong'); 32 } finally { 33 setLoading(false); 34 } 35 } 36 37 if (success) { 38 return ( 39 <div className="min-h-screen flex items-center justify-center p-4"> 40 <div className="w-full max-w-md text-center space-y-4"> 41 <div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto"> 42 <svg className="w-8 h-8 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 43 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> 44 </svg> 45 </div> 46 <h1 className="text-2xl font-bold">Check your email</h1> 47 <p className="text-gray-600"> 48 We sent a login link to <strong>{email}</strong> 49 </p> 50 <p className="text-sm text-gray-500"> 51 Click the link in the email to sign in. The link expires in 15 minutes. 52 </p> 53 <button 54 onClick={() => { setSuccess(false); setEmail(''); }} 55 className="text-blue-600 hover:underline text-sm" 56 > 57 Use a different email 58 </button> 59 </div> 60 </div> 61 ); 62 } 63 64 return ( 65 <div className="min-h-screen flex items-center justify-center p-4"> 66 <div className="w-full max-w-md space-y-8"> 67 <div className="text-center"> 68 <h1 className="text-2xl font-bold">Sign in</h1> 69 <p className="text-gray-600 mt-2"> 70 Enter your email and we'll send you a login link 71 </p> 72 </div> 73 74 {error && ( 75 <div className="p-4 bg-red-50 text-red-600 rounded-lg"> 76 {error} 77 </div> 78 )} 79 80 <form onSubmit={handleSubmit} className="space-y-4"> 81 <input 82 type="email" 83 value={email} 84 onChange={(e) => setEmail(e.target.value)} 85 placeholder="Enter your email" 86 required 87 className="w-full p-3 border rounded-lg" 88 /> 89 <button 90 type="submit" 91 disabled={loading} 92 className="w-full p-3 bg-blue-600 text-white rounded-lg disabled:opacity-50" 93 > 94 {loading ? 'Sending...' : 'Send login link'} 95 </button> 96 </form> 97 </div> 98 </div> 99 ); 100 }