3,209 قراءة٪ s
3,209 قراءة٪ s

تطبيق DIY Real-Time Polling يغلق الوصول مع Supabase و Permit.io

بواسطة Permit.io40m2025/04/16
Read on Terminal Reader

طويل جدا؛ ليقرأ

دليل كامل لإنشاء تطبيقات استطلاعات آمنة في الوقت الحقيقي مع الاعتماد، وإمكانات لكل مستخدم، والتحقق من السياسات الديناميكية باستخدام Supabase + Permit.io.
featured image - تطبيق DIY Real-Time Polling يغلق الوصول مع Supabase و Permit.io
Permit.io HackerNoon profile picture
0-item


بواسطةGabriel L. Manor


يساعد Supabase على إضافة الاعتماد إلى التطبيق الخاص بك بفضل الدعم المدمج لرسائل البريد الإلكتروني و OAuth و الروبوتات السحرية، ولكن في حين أن Supabase Auth يتعامل مع المستخدمين، غالبا ما تحتاج إلى طبقة الاعتماد أيضًا.


يوفر Supabase منصة دعم ممتازة مع auth وRow Level Security (RLS) المدمجة، وإدارةfine-grained permissionsوخاصة تلك التي تعتمد علىrelationships between users and dataبل بعيد عن السهل.


قد تحتاج إلى الحد من الأفعال مثل تعديل أو حذف البيانات لأصحاب الموارد، أو منع المستخدمين من التصويت على المحتوى الخاص بهم، أو تزويد الحقوق المختلفة على مكونات المستخدم المختلفة.


هذا التدريب يمر عبر كيفية تنفيذSupabase authentication and authorizationفي ANext.jsالتطبيق .

سنبدأ معSupabase Authلتسجيل الدخول وإدارة الجلسة ، ثم إضافةauthorization rulesاستخدامإدارة الوصول على أساس العلاقات (ReBAC)ويحرم من خلالSupabase Edge Functionsو alocal Policy Decision Point (PDP). .


في النهاية، سيكون لديك تطبيق بحث مشترك في الوقت الحقيقي الذي يدعم كل من العمليات العامة والمنشورة، وسيتم تطوير نظام التأشيرة مرنة مع نمو التطبيق الخاص بك.

ما نحن بصدده

في هذه الخطوة، سنقوم بتطوير تطبيق الاستفتاء في الوقت الحقيقي باستخدامSupabaseوNext.jsهذا يظهر كلا من الاعتماد والتوصية في العمل.


يتيح التطبيق للمستخدمين إنشاء الاستطلاعات، والانتخابات على الآخرين، وإدارة المحتوى فقط.Supabase Authلتسجيل الدخول / التسجيل وكيفية تطبيقهاauthorization policiesالذي يسيطر على من يمكنهم التصويت أو التعديل أو حذفها.


سوف نستخدم الميزات الأساسية لـ Supabase-Auth،Postgres،RLS،Realtimeو وEdge Functions- متوافقة مع ARelationship-Based Access Control (ReBAC)تطبيقات لتنفيذ قواعد الوصول لكل مستخدم و لكل مصدر.

تكنولوجيا Stack

  • Supabase – Backend-as-a-service لخدمات قاعدة البيانات، والموثوقية، والتطبيقات في الوقت الحقيقي، والصفحات
  • Next.js – نطاق Frontend لتطوير UI التطبيقات والطرق API
  • Permit.io – (بالإضافة إلى ReBAC) لتحديد وتقييم منطق التأمين من خلال PDP
  • Supabase CLI – لإدارة وتطوير وظائف Edge على المستوى المحلي والإنتاج
القائمةالتالي jsالمسمى.ioمكتبة CLI

متطلبات

  • Node.js تثبيت
  • محفظة حساب
  • إمكانية حساب
  • التعرف على React/Next.js
  • ابتداءً من مشروع Repo

ماذا يمكن أن يفعل هذا التطبيق؟

التطبيق الديموغرافي هو منصة الاستفتاء في الوقت الحقيقي التي تم إنشاؤها مع Next.js وSupabase ، حيث يمكن للمستخدمين إنشاء الاستفتاءات والانتخابات على الآخرين.


  • أي مستخدم (أو غير معتبر) يمكن رؤية قائمة الاستطلاعات العامة
  • فقط المستخدمين المقبولين يمكنهم إنشاء الاستطلاعات والتصويت
  • لا يمكن للمستخدمين التصويت على استطلاع يخلقونه
  • فقط إنشاء استطلاع يمكن إعداده أو حذفه

تقييم Tutorial

سوف نتبع هذه الخطوات العامة:


  1. إعداد مشروع Supabase ، schema ، auth ، و RLS
  2. بناء ميزات التطبيق الأساسية مثل إنشاء الاستطلاعات ووضع التصويت
  3. قواعد الوصية النموذجية تحدد الدور والقرارات في Permit.io
  4. إنشاء وظائف Supabase Edge لتنظيم المستخدمين، وتخصيص دورات، والتحقق من الحقوق
  5. تطبيق السياسات في مبنى التطبيقات باستخدام هذه الميزات الحدود


دعنا نبدأ -

Setting up Supabase in the Project

إنشاء Supabase في المشروع

تحديث: Clone the Starter Template

لقد خلقنا بالفعل aابتداء من المعبدذاكGitHubمع كل الكود الذي تحتاجه للبدء حتى يمكننا التركيز على تنفيذ Supabase و Permit.io.


يمكنك تشكيل المشروع من خلال إجراء الإجراءات التالية:


git clone <https://github.com/permitio/supabase-fine-grained-authorization>


بمجرد تكوين المشروع، قم بتنقل إلى مكتبة المشروع وتثبيت التوازنات:


cd realtime-polling-app-nextjs-supabase-permitio
npm install

إنشاء مشروع جديد في Supabase

للبدء :

  • اذهب إلى https://supabase.com و تسجيل الدخول أو إنشاء حساب.
  • انقر فوق "مشروع جديد" واضغط على اسم المشروع وكلمة المرور والمناطق.
  • بمجرد إنشاء، قم بإنشاء إعدادات المشروع → API وتسجيل عنوان المشروع الخاص بك و Anon Key - سوف تحتاجها في وقت لاحق.

تثبيت الوثائق والمعلومات في Supabase

سوف نستخدم البريد الإلكتروني المدمج / كلمة المرور Auth Supabase:

  • في صفحة الجانب، انقر فوق تصحيح → الموردين
  • تفعيل خدمة البريد الإلكتروني
  • (Optional) Disable email confirmation for testing, but keep it enabled for production


إنشاء خطة قاعدة البيانات

This app uses three main tables: polls،optionsو وvotes• استخدامهSQL Editorفي لوحة المفاتيح ، قم بإجراء التفاصيل التالية:


-- Create a polls table
CREATE TABLE polls (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    question TEXT NOT NULL,
    created_by UUID REFERENCES auth.users(id) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW()),
    creator_name TEXT NOT NULL,
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
);

-- Create an options table
CREATE TABLE options (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    poll_id UUID REFERENCES polls(id) ON DELETE CASCADE,
    text TEXT NOT NULL,
);

-- Create a votes table
CREATE TABLE votes (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    poll_id UUID REFERENCES polls(id) ON DELETE CASCADE,
    option_id UUID REFERENCES options(id) ON DELETE CASCADE,
    user_id UUID REFERENCES auth.users(id),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW()),
    UNIQUE(poll_id, user_id)
);

إمكانية حماية مستوى الشبكة (RLS)

تتيحRLSلكل جدول وتحديد السياسات:

-- Polls policies
ALTER TABLE polls ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Anyone can view polls" ON polls
    FOR SELECT USING (true);

CREATE POLICY "Authenticated users can create polls" ON polls
    FOR INSERT TO authenticated
    WITH CHECK (auth.uid() = created_by);

-- Options policies
ALTER TABLE options ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Anyone can view options" ON options
    FOR SELECT USING (true);

CREATE POLICY "Poll creators can add options" ON options
    FOR INSERT TO authenticated
    WITH CHECK (
        EXISTS (
            SELECT 1 FROM polls
            WHERE id = options.poll_id
            AND created_by = auth.uid()
        )
    );

-- Votes policies
ALTER TABLE votes ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Anyone can view votes" ON votes
    FOR SELECT USING (true);

CREATE POLICY "Authenticated users can vote once" ON votes
    FOR INSERT TO authenticated
    WITH CHECK (
        auth.uid() = user_id AND
        NOT EXISTS (
            SELECT 1 FROM polls
            WHERE id = votes.poll_id
            AND created_by = auth.uid()
        )
    );


لاستخدام ميزات Supabase في الوقت الحقيقي:

  • في صفحة الجانب ، قم بزيارة Table Editor
  • لكل منهما ثلاثة جدول (الانتخابات، الخيارات، التصويتات): انقر على ثلاثة نقاط → تعديل جدول الترجمة "تسمح في الوقت الحقيقي" حفظ التغييرات

تطبيق Supabase Email Authentication في التطبيق

في هذا التطبيق الديموغرافي، يمكن لأي شخص مشاهدة قائمة الاستطلاعات المتاحة على التطبيق، على حد سواء، النشطة والمتتالية.للتعرف على تفاصيل الاستطلاعات، وإدارة، أو التصويت على أي استطلاعات، يجب أن يكون المستخدم متصلًا.نحن سنستخدم البريد الإلكتروني وكلمة المرور كوسيلة للتأكد من هذا المشروع.في مشروع Next.js الخاص بك، تخزين شهادات Supabase الخاص بك في.env.local: :

NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key


تحديث عنصر التسجيل الخاص بك لتعامل مع التسجيل والتسجيل عبر البريد الإلكتروني / كلمة المرور:


import { useState } from "react";
import { createClient } from "@/utils/supabase/component";

const LogInButton = () => {
  const supabase = createClient();

  async function logIn() {
    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
    });
    if (error) {
      setError(error.message);
    } else {
      setShowModal(false);
    }
  }
  
  async function signUp() {
    const { error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        data: {
          user_name: userName,
        },
      },
    });
    if (error) {
      setError(error.message);
    } else {
      setShowModal(false);
    }
  }

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError("");

    if (isLogin) {
      await logIn();
    } else {
      await signUp();
    }
  };

  return (
    <>
      <button
        onClick={() => setShowModal(true)}
        className="flex items-center gap-2 p-2 bg-gray-800 text-white rounded-md">
        Log In
      </button>
      ...
    </>
  );
};

export default LogInButton;


هنا، نحن نستخدم SupabasesignInWithPasswordطريقة لتسجيل الدخول إلى المستخدم وsignUpطريقة لتسجيل المستخدم الجديد مع بريدك الإلكتروني وكلمة المرور. نحن أيضا تخزين اسم المستخدم فيuser_nameفي متاجر المستخدم.


يمكنك أيضًا استخدامsupabase.auth.signOut() to log users out and redirect them:


import { createClient } from "@/utils/supabase/component";
import { useRouter } from "next/router";

const LogOutButton = ({ closeDropdown }: { closeDropdown: () => void }) => {
  const router = useRouter();
  const supabase = createClient();

  const handleLogOut = async () => {
    await supabase.auth.signOut();
    closeDropdown();
    router.push("/");
  };
    
  return (
  ...
  );
};

export default LogOutButton;


هنا، نحن نستخدمsignOutطريقة من Supabase لإزالة المستخدم وتسجيلها إلى الصفحة الرئيسية.

الاستماع إلى التغييرات في حالة التأكد من المستخدم

تسمية التغييرات في حالة التأكد من المستخدم يتيح لنا تحديث UI بناءً على حالة التأكد من المستخدم.


  • عرض / إخفاء عناصر UI مثل زر login / logout
  • الحد من الوصول إلى صفحات محمية (مثل التصويت أو إدارة الاستطلاعات)
  • تأكد من أن المستخدمين الوحيدين يمكنهم القيام بعمليات محدودة


وسوف نستخدمsupabase.auth.onAuthStateChange()استمع إلى هذه الأحداث وتحديث التطبيق بهذه الطريقة.


In theLayout.tsxfile: Track Global Auth State


import React, { useEffect, useState } from "react";
import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";

const Layout = ({ children }: { children: React.ReactNode }) => {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    const fetchUser = async () => {
      const supabase = createClient();
      const { data } = supabase.auth.onAuthStateChange((event, session) => {
        setUser(session?.user || null);
      });

      return () => {
        data.subscription.unsubscribe();
      };
    };

    fetchUser();
  }, []);

  return (
    ...
  );
};

export default Layout;

الحد من الوصول إلى صفحات محمية

على صفحات مثلpoll detailsأوpoll managementيجب عليك أيضًا الاستماع إلى التغييرات في حالة التأكد من أن المستخدمين غير التأكد من التأكد من الوصول إليها.


هكذا تبدو فيpages/polls/[id].tsx: :

import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";

const Page = () => {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    const fetchUser = async () => {
      const supabase = createClient();
      const { data } = supabase.auth.onAuthStateChange((event, session) => {
        setUser(session?.user || null);
        setLoading(false);
      });

      return () => {
        data.subscription.unsubscribe();
      };
    };

    fetchUser();
  }, []);

  return (
    ...
  );

export default Page;


ويستخدم نموذج مماثل فيpages/polls/manage.tsxحيث ينبغي للمستخدمين رؤية الاستطلاعات الخاصة بهم فقط إذا كانوا متصلين:


import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";

const Page = () => {
  const [user, setUser] = useState<User | null>(null);
  
  const supabase = createClient();

  useEffect(() => {
    const fetchUser = async () => {
      const { data } = supabase.auth.onAuthStateChange((event, session) => {
        setUser(session?.user || null);
        if (!session?.user) {
          setLoading(false);
        }
      });

      return () => {
        data.subscription.unsubscribe();
      };
    };

    fetchUser();
  }, []);

  return (
    ...
  );
};

export default Page;


These patterns ensure your UI reflects the user’s current authentication status and form the basis for the authorization checks we'll add later. For example, you’ll later use this userالهدف عند الدعوة إلىcheckPermissionوظيفة Edge لتحديد ما إذا كان المستخدم يسمح بتصويت أو إدارة استطلاع معين.

Building the Polling App Functionality

مع Supabase تكوين وتأكيد العمل ، يمكننا الآن بناء الوظائف الأساسية لبرنامج الاستفتاء.


  • إنشاء استطلاعات جديدة
  • إرسال وإرسال الرسائل في الوقت الحقيقي
  • تطوير نظام التصويت


هذا يوفر لنا سلوك التطبيقات الأساسية التي سنحميها قريباً باستخدام الحقوق المحدودة.

إنشاء استطلاعات جديدة

يجب أن تكون المستخدمين متصلين لإنشاء الاستطلاعات. كل استطلاع يحتوي على سؤال، تاريخ انتهاء التكليف، ومجموعة من الخيارات. نحن أيضًا نشاهد من أطلقت الاستطلاعات حتى نتمكن في وقت لاحق من استخدام هذه العلاقة لمراقبة الوصول.


داخلNewPoll.tsx، احصل على المستخدم المقبول ، واستخدم Supabase لإدخال الاستطلاع وإختياراته:


import React, { useEffect, useState } from "react";
import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";

const NewPoll = () => {
  const [user, setUser] = useState<User | null>(null);
  
  const supabase = createClient();

  useEffect(() => {
    const fetchUser = async () => {
      const supabase = createClient();
      const { data } = supabase.auth.onAuthStateChange((event, session) => {
        setUser(session?.user || null);
      });

      return () => {
        data.subscription.unsubscribe();
      };
    };

    fetchUser();
  }, []);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (question.trim() && options.filter(opt => opt.trim()).length < 2) {
      setErrorMessage("Please provide a question and at least two options.");
      return;
    }

     // Create the poll
     const { data: poll, error: pollError } = await supabase
      .from("polls")
      .insert({
        question,
        expires_at: new Date(expiryDate).toISOString(),
        created_by: user?.id,
        creator_name: user?.user_metadata?.user_name,
      })
      .select()
      .single();

    if (pollError) {
      console.error("Error creating poll:", pollError);
      setErrorMessage(pollError.message);
      return;
    }
    
    // Create the options
    const { error: optionsError } = await supabase.from("options").insert(
      options
        .filter(opt => opt.trim())
        .map(text => ({
          poll_id: poll.id,
          text,
        }))
    );

    if (!optionsError) {
      setSuccessMessage("Poll created successfully!");
      handleCancel();
    } else {
      console.error("Error creating options:", optionsError);
      setErrorMessage(optionsError.message);
    }
  };

  return (
     ...
  );
};

export default NewPoll;

سنقوم في وقت لاحق بإدعاء وظيفة الحجم هنا لتخصيص دور "الإنتاج" في Permit.io.

إظهار وتصوير الاستطلاعات

يتم تقسيم الاستطلاعات إلىactive(لأنه لم يحدث بعد) وpast (expired). You can fetch them using Supabase queries filtered by the current timestamp, and set up real-time subscriptions to reflect changes instantly.


مثال منpages/index.tsx: :


import { PollProps } from "@/helpers";
import { createClient } from "@/utils/supabase/component";

export default function Home() {
  const supabase = createClient();

  useEffect(() => {
    const fetchPolls = async () => {
      setLoading(true);
      const now = new Date().toISOString();

      try {
        // Fetch active polls
        const { data: activePolls, error: activeError } = await supabase
          .from("polls")
          .select(
            `
            id,
            question,
            expires_at,
            creator_name,
            created_by,
            votes (count)
          `
          )
          .gte("expires_at", now)
          .order("created_at", { ascending: false });

        if (activeError) {
          console.error("Error fetching active polls:", activeError);
          return;
        }

        // Fetch past polls
        const { data: expiredPolls, error: pastError } = await supabase
          .from("polls")
          .select(
            `
            id,
            question,
            expires_at,
            creator_name,
            created_by,
            votes (count)
          `
          )
          .lt("expires_at", now)
          .order("created_at", { ascending: false });

        if (pastError) {
          console.error("Error fetching past polls:", pastError);
          return;
        }

        setCurrentPolls(activePolls);
        setPastPolls(expiredPolls);
      } catch (error) {
        console.error("Unexpected error fetching polls:", error);
      } finally {
        setLoading(false);
      }
    };

    fetchPolls();
    
        // Set up real-time subscription on the polls table:
    const channel = supabase
      .channel("polls")
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "polls",
        },
        fetchPolls
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, []);

  return (
    ...
  );
}

مشاهدة وإدارة استطلاعات المستخدمين

هنا ، نحن نتلقى النقاشات النشطة والخلفية منpollsكما أننا نحدد ترخيصًا في الوقت الحقيقي للتحدث عن التغييراتpolls table so that we can update the UI with the latest poll data. To differentiate between active and past polls, we are comparing the expiry date of each poll with the current date.


تحديث Thepages/manage.tsxصفحة لتسجيل وتشغيل الاستطلاعات فقط التي تم إنشاؤها من قبل المستخدم:


import { PollProps } from "@/helpers";

const Page = () => {
  
  useEffect(() => {
    if (!user?.id) return;

    const fetchPolls = async () => {
      try {
        const { data, error } = await supabase
          .from("polls")
          .select(
            `
            id,
            question,
            expires_at,
            creator_name,
            created_by,
            votes (count)
          `
          )
          .eq("created_by", user.id)
          .order("created_at", { ascending: false });

        if (error) {
          console.error("Error fetching polls:", error);
          return;
        }

        setPolls(data || []);
      } catch (error) {
        console.error("Unexpected error fetching polls:", error);
      } finally {
        setLoading(false);
      }
    };

    fetchPolls();

    // Set up real-time subscription
    const channel = supabase
      .channel(`polls_${user.id}`)
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "polls",
          filter: `created_by=eq.${user.id}`,
        },
        fetchPolls
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, [user]);

   return (
    ...
  );
};

export default Page;


هنا، نحن نبحث فقط عن الاستطلاعات التي تم إنشاؤها من قبل المستخدم، واستمع إلى التحديثات في الوقت الحقيقي.polls table so that the UI is updated with the latest poll data.


وبالتالي، تحديثPollCard component so that if a logged-in user is the poll creator, icons for editing and deleting the poll will be displayed to them on the poll.


import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";

const PollCard = ({ poll }: { poll: PollProps }) => {
  const [user, setUser] = useState<User | null>(null);
  
  useEffect(() => {
    const supabase = createClient();
    const fetchUser = async () => {
      const { data } = supabase.auth.onAuthStateChange((event, session) => {
        setUser(session?.user || null);
        setLoading(false);
      });

      return () => {
        data.subscription.unsubscribe();
      };
    };

    fetchUser();
  }, []);

  return (
    ...
      )}
    </Link>
  );
};

export default PollCard;


So now, on a poll card, if the logged-in user is the poll creator, icons for editing and deleting the poll will be displayed to them. This allows the user to manage only their polls.

تطوير نظام الاستفتاء

منطق التصويت يفرض:

  • 1 صوت لكل مستخدم
  • Creators cannot vote on their own polls
  • يتم تخزين التصويتات في جدول التصويت
  • Results are displayed and updated in real time


دعونا نلقي نظرة على كيف يعمل هذا فيViewPoll.tsx component:


Fetch the Logged-In Userنحن بحاجة إلى ID المستخدم الحالي لتحديد ملصقات التصويت وتسجيل التصويت.


import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";

const ViewPoll = () => {
  const [user, setUser] = useState<User | null>(null);

  const supabase = createClient();

  useEffect(() 
    const fetchUser = async () => {
      const {
        data: { user },
      } = await supabase.auth.getUser();
      setUser(user);
    };

    fetchUser();
  }, []);


Load Poll Details and Check Voting Statusبمجرد الحصول على المستخدم، نقوم بتحميل:

  • التقديرات نفسها (بما في ذلك الخيارات والتقديرات)
  • إذا كان هذا المستخدم قد صوت بالفعل


ونحن أيضا نسمع ذلك مرة أخرى في التحديثات في الوقت الحقيقي.


  useEffect(() => {
    if (!user) {
      return;
    }
    
    const checkUserVote = async () => {
      const { data: votes } = await supabase
        .from("votes")
        .select("id")
        .eq("poll_id", query.id)
        .eq("user_id", user.id)
        .single();

      setHasVoted(!!votes);
      setVoteLoading(false);
    };
   
    const fetchPoll = async () => {
      const { data } = await supabase
        .from("polls")
        .select(
          `
          *,
          options (
            id,
            text,
            votes (count)
          )
        `
        )
        .eq("id", query.id)
        .single();

      setPoll(data);
      setPollLoading(false);

      checkUserVote();
    };

    fetchPoll();


Listen for Real-Time Updates

ونحن نتفق مع التغييرات فيvotes table, scoped to this poll. When a new vote is cast, we fetch updated poll data and voting status.


    const channel = supabase
      .channel(`poll-${query.id}`)
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "votes",
          filter: `poll_id=eq.${query.id}`,
        },
        () => {
          fetchPoll();
          checkUserVote();
        }
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, [query.id, user]);


Handle the Vote Submission

If the user hasn’t voted and is allowed to vote (we’ll add a permission check later), we insert their vote.


  const handleVote = async (optionId: string) => {
    if (!user) return;

    try {
      const { error } = await supabase.from("votes").insert({
        poll_id: query.id,
        option_id: optionId,
        user_id: user.id,
      });

      if (!error) {
        setHasVoted(true);
      }
    } catch (error) {
      console.error("Error voting:", error);
    }
  };


Display the Poll ResultsWe calculate the total number of votes and a countdown to the expiration time. You can then use this to display progress bars or stats.


  if (!poll || pollLoading || voteLoading) return <div>Loading...</div>;

  // 6. calculate total votes
  const totalVotes = calculateTotalVotes(poll.options);
  const countdown = getCountdown(poll.expires_at);

  return (
  ...
  );
};

export default ViewPoll;


مع هذا الإعداد، نظام التصويت الخاص بك يعمل بشكل كامل. ولكن في الوقت الحالي، يمكن لأي شخص متصل أن يحاول تقنيًا التصويت - حتى في استطلاعاتهم الخاصة.authorization checks using Permit.ioوSupabase Edge Functionsلتنفيذ هذه القواعد.


قبل أن نفعل ذلك، دعونا نلقي نظرة على نوع طبقة التأشيرة التي سنستخدمها.

التعرف على ReBAC (مراقبة الوصول المرتبطة بالاتصال)

يعمل Supabase بشكل جيد مع الاعتماد على الوظائف الأساسية على مستوى الجانب، ولكنها لا تدعم قواعد معقدة مثل:

  • منع المستخدمين من التصويت على الاستطلاعات الخاصة بهم
  • تخصيص دورات لكل الموارد (مثل "المصمم" لبحث محدد)
  • Managing access via external policies


لمساعدة هذه الأنواع من الحقوق المرتبطة بالروابط ، سنستخدم ReBAC مع Permit.io.


Relationship-Based Access Control (ReBAC) is a model for managing permissions based on the relationships between users and resources. Instead of relying solely on roles or attributes (as in RBAC or ABAC), ReBAC determines access by evaluating how a user is connected to the resource they’re trying to access.

إدارة الوصول على أساس العلاقات (ReBAC)


في هذا التدريب ، نحن نطبق ReBAC على تطبيق الانتخابات:

  • يستطيع المستخدم الذي خلق استطلاع أن يسيطر عليه (إصلاح / إزالة)
  • لا يمكن للمستخدمين التصويت على الاستطلاع الخاص بهم
  • Other authenticated users can vote once per poll


من خلال نموذج هذه العلاقات في Permit.io ، يمكننا تحديد قواعد الوصول الخفيفة التي تتجاوز أمن مستوى الشبكة المدمج في Supabase (RLS).


لمزيد من المعلومات حول ReBAC ، اقرأأدوات ReBAC من Permit.io. .

تصميم التحكم في الوصول

بالنسبة لبرنامج الاستفتاء لدينا ، سنحدد:

  • الموارد واحدة مع العمليات المحددة على الموارد: الاستطلاعات: إنشاء، قراءة، إزالة، تحديث.
  • Two roles for granting permission levels based on a user’s relationship with the resources:
    • authenticated: Can perform create and read actions in polls. Can not delete, or update actions in polls.
    • creator: Can create, read, delete, and update actions in polls. Can perform read and create actions in votes. Cannot use create on their own polls.

إعداد Permit.io

Let’s walk through setting up the authorization model in Permit.

  • إنشاء مشروع جديد في Permit.io اسمها مثل supabase-polling
  • تحديد مصدر الاستطلاعات انقر فوق سياسة → قائمة الموارد انقر فوق "إنشاء مصدر" اسم مصدر الاستطلاعات، وإضافة العمليات: قراءة، إنشاء، تحديث، إزالة
  • تثبيت ReBAC للموارد تحت "مخترعات ReBAC" تحدد الدور التالي: إنشاء معتبر اضغط على حفظ
إنشاء مشروع جديد

يرجى ملاحظة أننا نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع أن نستطيع.admin،editor،userهذا لا يحتاج إلى هذا التدريب.


  • تحديد سياسات الوصول الى سياسات انقر فوق سياسة → سياسات استخدم الكاميرا المرئية لتحديد: يمكن قراءة الوثائق وتصنيعها من قبل مصمم الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الوثائق الو
  • ارفع عينات الموارد ارفع عينات المراجعة الفردية كعينات المراجعة الفردية (نتم تثبيت هذا في وقت لاحق عند إنشاء المراجعات الجديدة) ارفع عينات المراجعة إلى المستخدمين لكل استطلاع (على سبيل المثال، user123 هو المصمم من الاستطلاع456)

This structure gives us the power to write flexible access rules and enforce them per user, per poll.


الآن بعد الانتهاء من الإعدادات الأصلية على لوحة المساعدة ، دعونا نستخدمها في تطبيقنا.المسمى.ioإلى مشروع Supabase من خلال Edge Functions الذي:

  • Sync المستخدمين الجديدين
  • تخصيص دور المبدعين
  • التحقق من الوصول على الطلب

Setting up Permit in the Polling Application

يقدم Permit طرق متعددة لتكثيف التكامل مع التطبيق، ولكن سنستخدم Containers PDP لهذا التدريب. تحتاج إلى استضافة المكونات عبر الإنترنت لتسجيلها في وظائف Supabase Edge. يمكنك استخدام خدمات مثلطيران.comبمجرد تثبيتها ، قم بتخزين URL لملفك.


احصل على مفتاح API Permit الخاص بك عن طريق النقر على "مشاريع" في صفحة أدوات Permit، والتركيز إلى المشروع الذي أنتجته، والرد على الثلاث نقاط، واختيار "صورة مفتاح API".


إنشاء Supabase Edge Function API لتسمية

Supabase Edge Functionsهي مثالية للتكامل مع خدمات طرف ثالث مثل Permit.io. سوف نستخدمها لتنفيذ قواعد ReBAC الخاصة بنا في الوقت الفعلي من خلال التحقق من ما إذا كانت المستخدمين يسمحون بتنفيذ إجراءات معينة في الاستطلاعات.

Create Functions in Supabase

ابدأ Supabase في مشروعك وخلق ثلاث وظائف مختلفة باستخدامsupabase functions new command. These will be the starting point for your functions:

npx supabase init
npx supabase functions new syncUser
npx supabase functions new updateCreatorRole
npx supabase functions new checkPermission

وهذا سوف يخلقfunctionsالفوركس فيsupabaseأولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولاً: أولا

تداول الفوركس الخيارات الثنائية (syncUser.ts)

هذه الميزات تتحدث عن Supabase'sSIGNED_UPعندما يتم تسجيل المستخدم الجديد، ونحن نقوم بتسجيل هويتهم إلى Permit.io وتخصيصهم الحد الأدنىauthenticatedدورة


import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { Permit } from "npm:permitio";

 const corsHeaders = {
  'Access-Control-Allow-Origin': "*",
  'Access-Control-Allow-Headers': 'Authorization, x-client-info, apikey, Content-Type',
  'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
}

// Supabase Edge Function to sync new users with Permit.io
Deno.serve(async (req) => {

   const permit = new Permit({
    token: Deno.env.get("PERMIT_API_KEY"),
    pdp: "<https://real-time-polling-app-production.up.railway.app>",
   });
  
  try {
    const { event, user } = await req.json();

    // Only proceed if the event type is "SIGNED_UP"
    if (event === "SIGNED_UP" && user) {
      const newUser = {
        key: user.id,
        email: user.email,
        name: user.user_metadata?.name || "Someone",
      };

      // Sync the user to Permit.io
      await permit.api.createUser(newUser);
      await permit.api.assignRole({
        role: "authenticated",
        tenant: "default",
        user: user.id,
      });

      console.log(`User ${user.email} synced to Permit.io successfully.`);
    }

    // Return success response
    return new Response(
      JSON.stringify({ message: "User synced successfully!" }),
      { status: 200, headers: corsHeaders },
    );
  } catch (error) {
    console.error("Error syncing user to Permit: ", error);
    return new Response(
      JSON.stringify({
        message: "Error syncing user to Permit.",
        "error": error
      }),
      { status: 500, headers: { "Content-Type": "application/json" } },
    );
  }
});

تخصيص دور المبدع (updateCreatorRole.ts)

بمجرد أن يخلق المستخدم سؤالًا، يتم إرسال هذه الوظيفة إلى:

  • تنزيل الاستطلاع كواحدة من مصدر Permit.io الجديد
  • تخصيص الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة إلى الملفات المطروحة.

تقييم الأوراق المالية (checkPermission.ts)

هذه الوظيفة تعمل كمصممة - فإنه يحدد ما إذا كان المستخدم قادرًا على إجراء عملية معينة (create،read،update, delete(بالتحديد في زيارة معينة)


import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { Permit } from "npm:permitio";

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers":
    "Authorization, x-client-info, apikey, Content-Type",
  "Access-Control-Allow-Methods": "POST, GET, OPTIONS, PUT, DELETE",
};

Deno.serve(async req => {
  const permit = new Permit({
    token: Deno.env.get("PERMIT_API_KEY"),
    pdp: "<https://real-time-polling-app-production.up.railway.app>",
  });

  try {
    const { userId, operation, key } = await req.json();

    // Validate input parameters
    if (!userId || !operation || !key) {
      return new Response(
        JSON.stringify({ error: "Missing required parameters." }),
        { status: 400, headers: { "Content-Type": "application/json" } }
      );
    }

    // Check permissions using Permit's ReBAC
    const permitted = await permit.check(userId, operation, {
      type: "polls",
      key,
      tenant: "default",
      // Include any additional attributes that Permit needs for relationship checking
      attributes: {
        createdBy: userId, // This will be used in Permit's policy rules
      },
    });

    return new Response(JSON.stringify({ permitted }), {
      status: 200,
      headers: corsHeaders,
    });
  } catch (error) {
    console.error("Error checking user permission: ", error);

    return new Response(
      JSON.stringify({
        message: "Error occurred while checking user permission.",
        error: error,
      }),
      { status: 500, headers: { "Content-Type": "application/json" } }
    );
  }
});

اختبار محلي

بدء خادم تطوير Supabase الخاص بك للتحقق من الوظائف المحلية:

npx supabase start 
npx supabase functions serve

يمكنك بعد ذلك ضرب وظائفك في:

<http://localhost:54321/functions/v1/><function-name>

مثال :

<http://localhost:54321/functions/v1/checkPermission>

إدماج التحقق من التأشيرة في UI

Now that we’ve created our authorization logic with Permit.io and exposed it via Supabase Edge Functions, it’s time to enforce those checks inside the app’s components.


في هذه الفقرة، سنقوم بتحديث مكونات UI الأساسية لإدخال هذه الميزات وتسمح أو تعطى تدابير المستخدم مثل التصويت أو إدارة الاستطلاعات على أساس التحقق من الحقوق.

أندرويد.tsx: تخصيص دور المؤلف بعد إنشاء الاستطلاع

أندرويد.tsx

بعد إنشاء استطلاع وتخزينها إلى Supabase ، نحن نسميupdateCreatorRoleوظيفة 2 :

  • تنزيل الاستطلاع الجديد كميزة في Permit.io
  • وَقَالَ أَبُو حَنِيفَةَ وَالشَّافِعِيُّ وَالْحَاكِمُ وَالْحَاكِمُ وَالْحَاكِمُ وَالْبَيْهَقِيُّ وَالْبَيْهَقِيُّ وَالْبَيْهَقِيُّ وَالْبَيْهَقِيُّ وَالْبَيْهَقِيُّ وَالْبَيْهَقِيُّ وَالْبَيْهَقِيُّ وَالْحَاكِمُ وَالْبَيْهَقِيُّ وَالْبَيْهَقِيُّ وَ

ViewPoll.tsxالحد الأدنى من التصويت على أساس التأشيرة

قبل تمكين المستخدم من التصويت في استطلاعات الرأي، نقوم بإنشاءcheckPermissionوظيفة لتحديد ما إذا كان لديكcreateالترخيص علىvotesهذه هي الطريقة التي نقوم بها لتنفيذ القواعد:“A creator cannot vote on their own poll.”


Check voting permission:


const [canVote, setCanVote] = useState(false);

useEffect(() => {
  const checkPermission = async () => {
    if (!user || !query.id) return;

    try {
      const response = await fetch("<http://127.0.0.1:54321/functions/v1/checkPermission>", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          userId: user.id,
          operation: "create",
          key: query.id,
        }),
      });

      const { permitted } = await response.json();
      setCanVote(permitted);
    } catch (error) {
      console.error("Error checking permission:", error);
      setCanVote(false);
    }
  };

  checkPermission();
}, [user, query.id]);


Disable vote buttons if user isn’t allowed:


<button
  onClick={() => handleVote(option.id)}
  disabled={!user || !canVote}}
  className="w-full text-left p-4 rounded-md hover:bg-slate-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
  {option.text}
</button>


Show a message if the user is not allowed to vote:


{user && !canVote && (
  <p className="mt-4 text-gray-600">You cannot vote on your own poll</p>
)}

PollCard.tsx: Control Access to Edit/Delete

ونحن أيضًا نقوم بتحديد إجراءات إدارة الاستطلاعات (تعديل وإزالة) عن طريق التحقق من ما إذا كان المستخدم لديهupdateأوdeleteإجابة على هذا السؤال.


Check management permissions:


const [canManagePoll, setCanManagePoll] = useState(false);

useEffect(() => {
  const checkPollPermissions = async () => {
    if (!user || !poll.id) return;

    try {
      // Check for both edit and delete permissions
      const [editResponse, deleteResponse] = await Promise.all([
        fetch("<http://127.0.0.1:54321/functions/v1/checkPermission>", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            userId: user.id,
            operation: "update",
            key: poll.id,
          }),
        }),
        fetch("/api/checkPermission", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            userId: user.id,
            operation: "delete",
            key: poll.id,
          }),
        }),
      ]);

      const [{ permitted: canEdit }, { permitted: canDelete }] =
        await Promise.all([editResponse.json(), deleteResponse.json()]);

      // User can manage poll if they have either edit or delete permission
      setCanManagePoll(canEdit || canDelete);
    } catch (error) {
      console.error("Error checking permissions:", error);
      setCanManagePoll(false);
    }
  };

  checkPollPermissions();
}, [user, poll.id]);


Conditionally show management buttons:


استبدال :

{user?.id === poll?.created_by && (


مع :

{canManagePoll && (
  <div className="flex justify-start gap-4 mt-4">
    <button type="button" onClick={handleEdit}>
    </button>
    <button type="button" onClick={handleDelete}>
    </button>
  </div>
)}

اختبار التكامل

بمجرد دمجها ، يجب أن ترى السلوكات التالية في التطبيق:

  • يمكن للمستخدمين المتصلين رؤية الاستطلاعات ولكنهم لا يتفاعلون
  • يمكن للمستخدمين المؤهلين التصويت على الاستطلاعات التي لم تخلقها
  • المبدعين لا يمكنهم التصويت على الاستطلاعات الخاصة بهم
  • Only creators see edit/delete options on their polls


يجب أن تكون قادرًا على رؤية التغييرات في التطبيق من خلال الذهاب إلى المتصفح.في الشاشة الرئيسية، يمكن للمستخدمين رؤية قائمة الاستطلاعات النشطة والمتتالية، سواء كانت متصلة أو غير متصلة.


بعد تسجيل الدخول، يمكن للمستخدم مشاهدة تفاصيل الاستطلاع وتصحيحها، ومع ذلك، إذا كان المستخدم مصمم الاستطلاع، فلن يكون قادرًا على التصحيح.

النتيجة

في هذه الدورات، نستعرض كيفية تنفيذSupabase authentication and authorizationفي عالم حقيقيNext.jsالتطبيق .

بدأنا بإنشاءSupabase Auth for login and signup, created a relational schema with Row Level Security, and added dynamic authorization logic using ReBAC. With the help of Supabase Edge Functionsو aPolicy Decision Point (PDP)وبدأت عمليات التحقق من التأشيرة مباشرة من المفتشية.


من خلال الجمع بينSupabase Authمع التحكم المرونة في الوصول ، تمكننا من:

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


تتيح لك هذه الإعدادات أساساً قابلة للتوسع لإنشاء التطبيقات التي تتطلب أيضًا الاعتماد والتوصية الصغيرة.

اقرأ المزيد

  • Permit.io إرشادات ReBAC
  • إمكانية الحصول على تأشيرة + مؤشر
  • عناصر الترخيص: UI المدمج لإدارة الخصائص
  • تصفح البيانات مع الترخيص
  • حسابات التحقيقات

هل لديك أسئلة؟ الانضمام لدينامجتمع Slackحيث تقوم مئات المطورين بتصنيع وتناقش التمويل.

مجتمع Slack

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks