Kodax

Edge Functions — Supabase

Edge Functions

بعض المنطق لا يجوز أن يعمل في الواجهة: استدعاء API بمفتاح سري (الدفع، الذكاء الاصطناعي، البريد)، أو عمليات تحتاج صلاحيات كاملة، أو منطق يجب ألا يتلاعب به المستخدم. Edge Functions دوال TypeScript تعمل على خوادم Supabase (بيئة Deno).

أبسط دالة

// supabase/functions/hello/index.ts
Deno.serve(async (req) => {
  const { name } = await req.json().catch(() => ({ name: "زائر" }));
  return Response.json({ message: `مرحباً ${name}` });
});

Deno.serve هي الطريقة الحالية (الشروحات القديمة تستورد serve من deno.land/std — لم يعد ذلك لازماً).

الأوامر

npx supabase functions new hello
npx supabase functions serve hello            # محلياً
npx supabase secrets set RESEND_API_KEY=...   # الأسرار — لا تكتبها في الكود
npx supabase functions deploy hello

SUPABASE_URL و SUPABASE_ANON_KEY و SUPABASE_SERVICE_ROLE_KEY متاحة تلقائياً بـ Deno.env.get.

الهوية أولاً

افتراضياً تتطلب الدوال رمز JWT صالحاً. داخل الدالة، اعرف من المستخدم قبل أي عمل:

import { createClient } from "npm:@supabase/supabase-js@2";

Deno.serve(async (req) => {
  const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!, {
    global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } },
  });
  const { data: { user }, error } = await supabase.auth.getUser();
  if (error || !user) return Response.json({ error: "غير مصرح" }, { status: 401 });
  const { data } = await supabase.from("todos").select("*");   // RLS تُطبق بهوية المستخدم
  return Response.json({ user: user.id, todos: data });
});

- عميل بمفتاح anon + رمز المستخدم: RLS تُطبق — الخيار الافتراضي.

- عميل بـ service_role: يتجاوز RLS — فقط لعمليات إدارية محددة بعد التحقق بنفسك من هوية المستخدم وصلاحيته.

منع إساءة الاستخدام

دالة ترسل بريداً لأي عنوان وأي محتوى يحددهما الطلب = منصة إرسال رسائل مزعجة وتصيد باسم نطاقك. القواعد:

- تحقق من المستخدم وصلاحيته.

- حدد المستلم من بياناتك (بريد المستخدم نفسه، أو قائمة ثابتة)، لا من الطلب.

- قوالب محتوى ثابتة؛ لا HTML حر من العميل.

- حد للطلبات لكل مستخدم.

CORS

إذا استُدعيت الدالة من المتصفح بنطاق مختلف، أجب على طلب OPTIONS وأضف ترويسات Access-Control-Allow-* بنطاقك تحديداً. supabase.functions.invoke() من التطبيق يضيف الرمز تلقائياً.

تمرين

اكتب دالة contact-support تسمح للمستخدم المسجل بإرسال رسالة للدعم فقط: المستلم ثابت في الأسرار، والنص يُحفظ في جدول ويُرسل كنص عادي (لا HTML)، مع حد 5 رسائل يومياً لكل مستخدم.

// supabase/functions/contact-support/index.ts
// ينشر بـ: npx supabase functions deploy contact-support
// الأسرار: npx supabase secrets set RESEND_API_KEY=... SUPPORT_EMAIL=support@example.com
import { createClient } from "npm:@supabase/supabase-js@2";

const ALLOWED_ORIGIN = "https://app.example.com";
const cors = {
  "Access-Control-Allow-Origin": ALLOWED_ORIGIN,
  "Access-Control-Allow-Headers": "authorization, content-type, apikey, x-client-info",
  "Access-Control-Allow-Methods": "POST, OPTIONS",
};

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") return new Response(null, { headers: cors });
  if (req.method !== "POST") return Response.json({ error: "POST فقط" }, { status: 405, headers: cors });

  // 1) من المستخدم؟ عميل بمفتاح anon ورمز المستخدم ← RLS تُطبق
  const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!, {
    global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } },
  });
  const { data: { user }, error: authError } = await supabase.auth.getUser();
  if (authError || !user) return Response.json({ error: "سجّل الدخول أولاً" }, { status: 401, headers: cors });

  // 2) التحقق من المدخلات
  const body = await req.json().catch(() => null);
  const message = typeof body?.message === "string" ? body.message.trim() : "";
  if (message.length < 10 || message.length > 2000) {
    return Response.json({ error: "الرسالة بين 10 و 2000 حرف" }, { status: 400, headers: cors });
  }

  // 3) حد يومي: سياسة RLS تسمح للمستخدم بقراءة رسائله فقط
  const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
  const { count } = await supabase.from("support_messages").select("id", { count: "exact", head: true }).gte("created_at", since);
  if ((count ?? 0) >= 5) return Response.json({ error: "تجاوزت الحد اليومي" }, { status: 429, headers: cors });

  const { error: insertError } = await supabase.from("support_messages").insert({ message });   // user_id = auth.uid() افتراضياً
  if (insertError) return Response.json({ error: "تعذر الحفظ" }, { status: 500, headers: cors });

  // 4) المستلم ثابت من الأسرار، والمحتوى نص عادي — لا مستلم ولا HTML من الطلب
  const res = await fetch("https://api.resend.com/emails", {
    method: "POST",
    headers: { Authorization: `Bearer ${Deno.env.get("RESEND_API_KEY")}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      from: "Kodax <noreply@example.com>",
      to: Deno.env.get("SUPPORT_EMAIL"),
      subject: `رسالة دعم من ${user.email}`,
      text: message,
    }),
  });
  if (!res.ok) return Response.json({ error: "تعذر الإرسال" }, { status: 502, headers: cors });

  return Response.json({ ok: true }, { headers: cors });
});
اختبر فهمك