Loading...
Loading...
> Passwordless authentication via email magic links for frictionless sign-in.
Magic links provide passwordless authentication by sending a unique, time-limited link to the user's email. Features include one-click sign-in from email, no password to remember or manage, automatic email verification, time-limited tokens (24 hours default), and single-use links for security.
DESC: Enable the feature in src/config.js
1export const config = {2 features: {3 magicLinks: true,4 // ...other features5 },6};
DESC: Ensure email is configured in .env.local
1$RESEND_API_KEY="re_xxxxxxxxxxxx"2$EMAIL_FROM="Your App <noreply@yourdomain.com>"3$NEXT_PUBLIC_APP_URL="http://localhost:3000"
DESC: Configure token expiration
1export const config = {2 auth: {3 magicLinkExpiry: 24 * 60 * 60 * 1000, // 24 hours in ms4 // ...5 },6};
Create the API endpoint at /api/auth/magic-link
1// src/app/api/auth/magic-link/route.ts2import { NextResponse } from "next/server";3import { prisma } from "@/lib/db";4import { sendMagicLinkEmail } from "@/lib/email";5import { nanoid } from "nanoid";6import { config } from "@/config";78export async function POST(request: Request) {9 const { email } = await request.json();1011 if (!email) {12 return NextResponse.json({ error: "Email required" }, { status: 400 });13 }1415 // Find or create user16 let user = await prisma.user.findUnique({17 where: { email },18 });1920 if (!user) {21 user = await prisma.user.create({22 data: {23 email,24 emailVerified: null, // Will be verified on click25 },26 });27 }2829 // Generate secure token30 const token = nanoid(32);31 const expires = new Date(Date.now() + config.auth.magicLinkExpiry);3233 // Store token34 await prisma.verificationToken.create({35 data: {36 identifier: email,37 token,38 expires,39 },40 });4142 // Send magic link email43 const magicLink = `${config.app.url}/api/auth/verify?token=${token}&email=${email}`;4445 await sendMagicLinkEmail({46 to: email,47 magicLink,48 expiresIn: "24 hours",49 });5051 return NextResponse.json({52 success: true,53 message: "Magic link sent to your email",54 });55}
Client-side form component
1"use client";23import { useState } from "react";4import { Button } from "@/components/ui/button";5import { Input } from "@/components/ui/input";67export function MagicLinkForm() {8 const [email, setEmail] = useState("");9 const [loading, setLoading] = useState(false);10 const [sent, setSent] = useState(false);1112 const handleSubmit = async (e: React.FormEvent) => {13 e.preventDefault();14 setLoading(true);1516 try {17 const response = await fetch("/api/auth/magic-link", {18 method: "POST",19 headers: { "Content-Type": "application/json" },20 body: JSON.stringify({ email }),21 });2223 if (!response.ok) {24 throw new Error("Failed to send magic link");25 }2627 setSent(true);28 } catch (_) {29 alert("Failed to send magic link. Please try again.");30 } finally {31 setLoading(false);32 }33 };3435 if (sent) {36 return (37 <div className="text-center py-8">38 <h3>Check your email</h3>39 <p>We sent a magic link to <strong>{email}</strong></p>40 <p>Click the link in the email to sign in.</p>41 </div>42 );43 }4445 return (46 <form onSubmit={handleSubmit} className="space-y-4">47 <Input48 type="email"49 placeholder="you@example.com"50 value={email}51 onChange={(e) => setEmail(e.target.value)}52 required53 />54 <Button type="submit" className="w-full" disabled={loading}>55 {loading ? "Sending..." : "Send Magic Link"}56 </Button>57 </form>58 );59}