Sihirli Bağlantılar

E-posta sihirli bağlantılarıyla şifresiz kimlik doğrulama.

Not: Bu içerik, yalnızca gösterim amaçlı örnek/yer tutucu içeriktir.

Sihirli bağlantılar, kullanıcının e-posta adresine tek kullanımlık bir bağlantı göndererek şifresiz kimlik doğrulama sağlar.

Nasıl Çalışır?

  1. Kullanıcı e-posta adresini girer
  2. Sistem benzersiz bir bağlantı içeren bir e-posta gönderir
  3. Kullanıcı e-postasındaki bağlantıya tıklar
  4. Kullanıcının oturumu otomatik olarak açılır

Avantajlar

  • Hatırlanacak şifre yok - Daha iyi kullanıcı deneyimi
  • Daha güvenli - Çalınacak şifre yok
  • Daha az sürtünme - Daha hızlı kayıt süreci
  • E-posta doğrulaması - E-posta sahipliğini doğrular

Uygulama

Sihirli Bağlantı Formu

'use client';

import { useForm } from 'react-hook-form';
import { sendMagicLinkAction } from '../_lib/actions';

export function MagicLinkForm() {
  const { register, handleSubmit, formState: { isSubmitting } } = useForm();
  const [sent, setSent] = useState(false);

  const onSubmit = async (data) => {
    const result = await sendMagicLinkAction(data);

    if (result.success) {
      setSent(true);
    }
  };

  if (sent) {
    return (
      <div className="text-center">
        <h2>Check your email</h2>
        <p>We've sent you a magic link to sign in.</p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>Email address</label>
        <input
          type="email"
          {...register('email', { required: true })}
          placeholder="you@example.com"
        />
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Sending...' : 'Send magic link'}
      </button>
    </form>
  );
}

Server Action

'use server';

import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { z } from 'zod';

export const sendMagicLinkAction = enhanceAction(
  async (data) => {
    const client = getSupabaseServerClient();
    const origin = process.env.NEXT_PUBLIC_SITE_URL!;

    const { error } = await client.auth.signInWithOtp({
      email: data.email,
      options: {
        emailRedirectTo: `${origin}/auth/callback`,
        shouldCreateUser: true,
      },
    });

    if (error) throw error;

    return {
      success: true,
      message: 'Check your email for the magic link',
    };
  },
  {
    schema: z.object({
      email: z.string().email(),
    }),
  }
);

Yapılandırma

Supabase'de Etkinleştirme

  1. Authentication → Providers → Email bölümüne gidin
  2. "Enable Email Provider" seçeneğini etkinleştirin
  3. "Enable Email Confirmations" seçeneğini etkinleştirin

E-posta Şablonunu Yapılandırma

Sihirli bağlantı e-postasını Supabase Dashboard'da özelleştirin:

  1. Authentication → Email Templates bölümüne gidin
  2. "Magic Link" seçeneğini seçin
  3. Şablonu özelleştirin:
<h2>Sign in to {{ .SiteURL }}</h2>
<p>Click the link below to sign in:</p>
<p><a href="{{ .ConfirmationURL }}">Sign in</a></p>
<p>This link expires in {{ .TokenExpiryHours }} hours.</p>

Callback İşleyicisi

Sihirli bağlantı callback'ini işleyin:

// app/auth/callback/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const requestUrl = new URL(request.url);
  const token_hash = requestUrl.searchParams.get('token_hash');
  const type = requestUrl.searchParams.get('type');

  if (token_hash && type === 'magiclink') {
    const cookieStore = cookies();
    const supabase = createRouteHandlerClient({ cookies: () => cookieStore });

    const { error } = await supabase.auth.verifyOtp({
      token_hash,
      type: 'magiclink',
    });

    if (!error) {
      return NextResponse.redirect(new URL('/home', request.url));
    }
  }

  // Doğrulama başarısız olursa hata döndür
  return NextResponse.redirect(
    new URL('/auth/sign-in?error=invalid_link', request.url)
  );
}

Gelişmiş Özellikler

Özel Yönlendirme

Kullanıcıların bağlantıya tıkladıktan sonra nereye gideceğini belirtin:

await client.auth.signInWithOtp({
  email: data.email,
  options: {
    emailRedirectTo: `${origin}/onboarding`,
  },
});

Otomatik Kaydı Devre Dışı Bırakma

Kullanıcıların önce kaydolmasını zorunlu kılın:

await client.auth.signInWithOtp({
  email: data.email,
  options: {
    shouldCreateUser: false, // Yeni kullanıcı oluşturma
  },
});

Token Süresi

Bağlantının geçerlilik süresini yapılandırın (varsayılan: 1 saat):

-- Supabase SQL Editor içinde
ALTER TABLE auth.users
SET default_token_lifetime = '15 minutes';

Hız Sınırlaması

Sihirli bağlantı isteklerine hız sınırı uygulayarak kötüye kullanımı önleyin:

import { ratelimit } from '~/lib/rate-limit';

export const sendMagicLinkAction = enhanceAction(
  async (data, user, request) => {
    // IP adresine göre hız sınırı uygula
    const ip = request.headers.get('x-forwarded-for') || 'unknown';
    const { success } = await ratelimit.limit(ip);

    if (!success) {
      throw new Error('Too many requests. Please try again later.');
    }

    const client = getSupabaseServerClient();

    await client.auth.signInWithOtp({
      email: data.email,
    });

    return { success: true };
  },
  { schema: EmailSchema }
);

Güvenlik Hususları

Bağlantı Süresinin Dolması

Sihirli bağlantıların süresi kısa sürede dolmalıdır:

  • Varsayılan: 1 saat
  • Önerilen: üretim ortamı için 15-30 dakika
  • Hassas işlemler için daha kısa

Tek Kullanımlık

Bağlantılar kullanıldıktan sonra geçersiz kılınmalıdır:

// Supabase bunu otomatik olarak yönetir
// Her bağlantı yalnızca bir kez kullanılabilir

E-posta Doğrulaması

E-postaların doğrulandığından emin olun:

const { data: { user } } = await client.auth.getUser();

if (!user.email_confirmed_at) {
  redirect('/verify-email');
}

Kullanıcı Deneyimi

Yükleme Durumu

Gönderim sırasında geri bildirim gösterin:

export function MagicLinkForm() {
  const [status, setStatus] = useState<'idle' | 'sending' | 'sent'>('idle');

  const onSubmit = async (data) => {
    setStatus('sending');
    await sendMagicLinkAction(data);
    setStatus('sent');
  };

  return (
    <>
      {status === 'idle' && <EmailForm onSubmit={onSubmit} />}
      {status === 'sending' && <SendingMessage />}
      {status === 'sent' && <CheckEmailMessage />}
    </>
  );
}

Bağlantıyı Yeniden Gönderme

Kullanıcıların yeni bir bağlantı talep etmesine izin verin:

export function ResendMagicLink({ email }: { email: string }) {
  const [canResend, setCanResend] = useState(false);
  const [countdown, setCountdown] = useState(60);

  useEffect(() => {
    if (countdown > 0) {
      const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
      return () => clearTimeout(timer);
    } else {
      setCanResend(true);
    }
  }, [countdown]);

  const handleResend = async () => {
    await sendMagicLinkAction({ email });
    setCountdown(60);
    setCanResend(false);
  };

  return (
    <button onClick={handleResend} disabled={!canResend}>
      {canResend ? 'Resend link' : `Resend in ${countdown}s`}
    </button>
  );
}

E-posta Teslim Edilebilirliği

SPF, DKIM, DMARC

E-posta kimlik doğrulamasını yapılandırın:

  1. DNS'e SPF kaydı ekleyin
  2. DKIM imzalamayı etkinleştirin
  3. DMARC politikası oluşturun

Özel E-posta Alan Adı

Daha iyi teslim edilebilirlik için kendi alan adınızı kullanın:

  1. Project Settings → Auth bölümüne gidin
  2. Özel SMTP yapılandırın
  3. Alan adı sahipliğini doğrulayın

Geri Dönen E-postaları İzleme

E-posta teslimat sorunlarını takip edin:

// Geri dönen e-postaları işle
export async function handleEmailBounce(email: string) {
  await client.from('email_bounces').insert({
    email,
    bounced_at: new Date(),
  });

  // Kullanıcıyı başka bir kanal üzerinden bilgilendir
}

Test

Yerel Geliştirme

Geliştirme ortamında e-postalar InBucket'a gider:

http://localhost:54324

Test sırasında sihirli bağlantı e-postalarını görmek için bu URL'yi kontrol edin.

Test Modu

E-posta göndermeden bir test bağlantısı oluşturun:

if (process.env.NODE_ENV === 'development') {
  console.log('Magic link URL:', confirmationUrl);
}

En İyi Uygulamalar

  1. Açık iletişim - Kullanıcılara spam klasörünü kontrol etmelerini söyleyin
  2. Kısa geçerlilik süresi - Güvenlik için 15-30 dakika
  3. Hız sınırlaması - Kötüye kullanımı önleyin
  4. Yedek seçenek - Yedek olarak şifreyle kimlik doğrulama sunun
  5. Özel alan adı - Daha iyi teslim edilebilirlik
  6. Teslimatı izleyin - Geri dönen ve başarısız e-postaları takip edin
  7. Yeniden gönderme seçeneği - Kullanıcıların yeni bağlantı talep etmesine izin verin
  8. Mobil uyumluluk - Bağlantıların mobil cihazlarda çalıştığından emin olun