الروابط السحرية

مصادقة بدون كلمة مرور باستخدام الروابط السحرية عبر البريد الإلكتروني.

ملاحظة: هذا محتوى تجريبي/مؤقت لأغراض العرض التوضيحي.

توفر الروابط السحرية مصادقة بدون كلمة مرور عبر إرسال رابط يُستخدم لمرة واحدة إلى البريد الإلكتروني للمستخدم.

آلية العمل

  1. يُدخل المستخدم عنوان بريده الإلكتروني
  2. يُرسل النظام رسالة بريد إلكتروني تحتوي على رابط فريد
  3. ينقر المستخدم على الرابط الموجود في بريده الإلكتروني
  4. يُسجَّل دخول المستخدم تلقائيًا

المزايا

  • لا كلمة مرور لتذكّرها - تجربة مستخدم أفضل
  • أمان أعلى - لا توجد كلمة مرور يمكن سرقتها
  • عوائق أقل - عملية إنشاء حساب أسرع
  • التحقق من البريد الإلكتروني - يؤكد ملكية البريد الإلكتروني

التنفيذ

نموذج الرابط السحري

'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>
  );
}

إجراء الخادم

'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(),
    }),
  }
);

الإعداد

التفعيل في Supabase

  1. انتقل إلى Authentication ← Providers ← Email
  2. فعّل خيار «Enable Email Provider»
  3. فعّل خيار «Enable Email Confirmations»

إعداد قالب البريد الإلكتروني

خصّص رسالة الرابط السحري في لوحة تحكم Supabase:

  1. انتقل إلى Authentication ← Email Templates
  2. اختر «Magic Link»
  3. خصّص القالب:
<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>

معالج الاستدعاء العكسي

تعامل مع الاستدعاء العكسي للرابط السحري:

// 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));
    }
  }

  // إرجاع خطأ في حال فشل التحقق
  return NextResponse.redirect(
    new URL('/auth/sign-in?error=invalid_link', request.url)
  );
}

الميزات المتقدمة

إعادة توجيه مخصصة

حدّد الوجهة التي ينتقل إليها المستخدمون بعد النقر على الرابط:

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

تعطيل إنشاء الحساب التلقائي

اشترط أن ينشئ المستخدمون حساباتهم أولًا:

await client.auth.signInWithOtp({
  email: data.email,
  options: {
    shouldCreateUser: false, // عدم إنشاء مستخدمين جدد
  },
});

انتهاء صلاحية الرمز

اضبط مدة صلاحية الرابط (الافتراضية: ساعة واحدة):

-- في محرر SQL الخاص بـ Supabase
ALTER TABLE auth.users
SET default_token_lifetime = '15 minutes';

تحديد معدل الطلبات

امنع إساءة الاستخدام عبر تحديد معدل طلبات الروابط السحرية:

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

export const sendMagicLinkAction = enhanceAction(
  async (data, user, request) => {
    // تحديد المعدل حسب عنوان IP
    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 }
);

اعتبارات أمنية

انتهاء صلاحية الرابط

يجب أن تنتهي صلاحية الروابط السحرية بسرعة:

  • الافتراضي: ساعة واحدة
  • الموصى به: من 15 إلى 30 دقيقة في بيئة الإنتاج
  • مدة أقصر للإجراءات الحساسة

الاستخدام لمرة واحدة

يجب إبطال الروابط بعد استخدامها:

// تتولى Supabase ذلك تلقائيًا
// لا يمكن استخدام كل رابط إلا مرة واحدة

التحقق من البريد الإلكتروني

تأكد من التحقق من عناوين البريد الإلكتروني:

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

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

تجربة المستخدم

حالة التحميل

اعرض ملاحظات للمستخدم أثناء الإرسال:

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 />}
    </>
  );
}

إعادة إرسال الرابط

اسمح للمستخدمين بطلب رابط جديد:

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>
  );
}

قابلية تسليم البريد الإلكتروني

SPF وDKIM وDMARC

اضبط مصادقة البريد الإلكتروني:

  1. أضف سجل SPF إلى DNS
  2. فعّل توقيع DKIM
  3. أعدّ سياسة DMARC

نطاق بريد إلكتروني مخصص

استخدم نطاقك الخاص لتحسين قابلية التسليم:

  1. انتقل إلى Project Settings ← Auth
  2. اضبط خادم SMTP مخصصًا
  3. تحقق من ملكية النطاق

مراقبة الرسائل المرتدة

تتبّع مشكلات تسليم البريد الإلكتروني:

// التعامل مع رسائل البريد الإلكتروني المرتدة
export async function handleEmailBounce(email: string) {
  await client.from('email_bounces').insert({
    email,
    bounced_at: new Date(),
  });

  // إخطار المستخدم عبر قناة أخرى
}

الاختبار

التطوير المحلي

في بيئة التطوير، تُرسل رسائل البريد الإلكتروني إلى InBucket:

http://localhost:54324

راجع هذا العنوان للاطلاع على رسائل الروابط السحرية أثناء الاختبار.

وضع الاختبار

أنشئ رابطًا تجريبيًا دون إرسال بريد إلكتروني:

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

أفضل الممارسات

  1. تواصل واضح - اطلب من المستخدمين التحقق من مجلد الرسائل غير المرغوب فيها
  2. مدة صلاحية قصيرة - من 15 إلى 30 دقيقة لدواعٍ أمنية
  3. تحديد معدل الطلبات - لمنع إساءة الاستخدام
  4. خيار بديل - وفّر المصادقة بكلمة المرور كخيار احتياطي
  5. نطاق مخصص - لتحسين قابلية التسليم
  6. مراقبة التسليم - تتبّع الرسائل المرتدة وحالات الفشل
  7. خيار إعادة الإرسال - اسمح للمستخدمين بطلب رابط جديد
  8. ملاءمة الأجهزة المحمولة - تأكد من أن الروابط تعمل على الأجهزة المحمولة