1,843 показания
1,843 показания

DIY Real-Time Polling App блокира достъпа с Supabase и Permit.io

от Permit.io40m2025/04/16
Read on Terminal Reader

Твърде дълго; Чета

Пълно ръководство за изграждане на безопасно приложение за гласуване в реално време с удостоверяване, разрешения на потребител и динамични проверки на правилата – с помощта на Supabase + Permit.io.
featured image - DIY Real-Time Polling App блокира достъпа с Supabase и Permit.io
Permit.io HackerNoon profile picture
0-item


отGabriel L. Manor


Supabase улеснява добавянето на удостоверяване към приложението с вградена поддръжка за имейл, OAuth и магически връзки.Но докато Supabase Auth се занимава с това кои са вашите потребители, често се нуждаете и от разрешителен слой.


Supabase предлага страхотен backend с вградена Auth и Row Level Security (RLS), управлениеfine-grained permissionsОсобено тези, които се основават наrelationships between users and dataДалеч е от лесно.


Може да искате да ограничите действия като редактиране или изтриване на данни до собствениците на ресурси, да предотвратите потребителите да гласуват за собственото си съдържание или да наложите различни разрешения за различни потребителски роли.


Този урок разказва как да се прилагаSupabase authentication and authorizationВ АNext.jsна приложението.

Ще започнем сSupabase Authза регистрация и управление на сесията, след това добаветеauthorization rulesИзползванеРелационен контрол на достъпа (ReBAC)Налага се чрезSupabase Edge Functionsи аlocal Policy Decision Point (PDP). от


В края на краищата ще имате приложение за съвместно гласуване в реално време, което поддържа както публични, така и защитени действия – и гъвкава система за разрешаване, която можете да развиете, докато приложението ви расте.

Какво изграждаме

В това ръководство ще изградим приложение за гласуване в реално време, използващоSupabaseиNext.jsТова показва както автентичността, така и авторизацията в действие.


Приложението позволява на потребителите да създават проучвания, да гласуват за други и да управляват само собственото си съдържание.Supabase Authза login/signup и как да се прилагаauthorization policiesкойто контролира кой може да гласува, редактира или изтрива.


Ще използваме основните функции на Supabase -Auth,Postgres,RLS,RealtimeиEdge FunctionsВ комбинация с аRelationship-Based Access Control (ReBAC)Модел за прилагане на правилата за достъп на потребител и на ресурс.

Технически стак

    на
  • Supabase – Backend-as-a-service за бази данни, удостоверяване, функции в реално време и Edge
  • на
  • Next.js – Frontend Framework за изграждане на приложения UI и API маршрути
  • на
  • Permit.io – (за ReBAC) за определяне и оценка на логиката на разрешаване чрез PDP
  • на
  • Създаване на клип – To manage and deploy Edge Functions locally and in production
  • на
СубазаСледваща .jsРазрешителноСъздаване на клип

Предпоставки

    на
  • Node.js е инсталиран
  • на
  • Счетоводна база
  • на
  • Разреши.io акаунт
  • на
  • Използване на React/Next.js
  • на
  • Стартиране на проекта Repo
  • на

Какво може да направи това приложение?

Демо-приложението е платформа за гласуване в реално време, изградена с Next.js и Supabase, където потребителите могат да създават анкети и да гласуват за други.


    на
  • Всеки потребител (автентичен или не) може да преглежда списъка с обществените проучвания
  • на
  • Само автентични потребители могат да създават анкети и да гласуват
  • на
  • Потребителят не може да гласува в анкета, която е създал
  • на
  • Само създателят на анкетата може да го редактира или изтрие
  • на

Tutorial Преглед

Ще следваме тези общи стъпки:


    на
  1. Настройване на Supabase проект, схема, auth и RLS
  2. на
  3. Изграждане на основни функции на приложението, като например създаване на проучвания и гласуване
  4. на
  5. Правилата за модел на разрешение определят ролите и правилата в Permit.io
  6. на
  7. Създаване на функции на Supabase Edge за синхронизиране на потребители, възлагане на роли и проверка на разрешения
  8. на
  9. Прилагане на политики в приложението с помощта на тези функции Edge
  10. на


Нека да започнем -

Setting up Supabase in the Project

Създаване на база данни в проекта

Опционално: Clone the Starter Template

Вече съм създал еднаНачалото на храмае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 и отбеляжете вашия URL адрес на проекта и Anon Key – ще ви трябват по-късно.
  • на

Настройване на удостоверяване и база данни в Supabase

Ще използваме вградения имейл/парола на Supabase:

    на
  • В страничната лента отидете на Автентичност → Доставчици
  • на
  • Активирайте имейл доставчика
  • на
  • (Необходимо) Деактивирайте имейл потвърждение за тестване, но го запазете включен за производство
  • на

Създаване на схема на базата данни

Това приложение използва три основни таблици:polls,optionsиvotes• ИзползвайтеSQL Editorв таблото на Supabase и изпълнете следното:


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

ПозволяваРЛСза всяка таблица и определяне на политики:

-- 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
  • на
  • За всяка от трите таблици (попитания, опции, гласове): Кликнете върху трите точки → Редактиране на таблица Toggle "Enable Realtime" Запиши промените
  • на

Използване на Supabase Email Authentication в приложението

В това демо приложение всеки може да преглежда списъка с наличните в приложението проучвания, както активни, така и изтекли.За да преглеждате подробностите за проучване, да управлявате или да гласувате за всяко проучване, потребителят трябва да е влязъл.Ние ще използваме имейл и парола като средство за удостоверяване за този проект..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()За да изключите потребителите и да ги пренасочите:


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 да се отпишете от потребителя и да ги пренасочите към началната страница.

Слушане за промени в състоянието на удостоверяване на потребителя

Слушането на промени в състоянието на удостоверяване на потребителя ни позволява да актуализираме потребителския интерфейс въз основа на състоянието на удостоверяване на потребителя.


    на
  • Показване/скриване на елементи на потребителския интерфейс като бутони за вход/вход
  • на
  • Условно ограничаване на достъпа до защитени страници (като гласуване или управление на проучвания)
  • на
  • Уверете се, че само авторизирани потребители могат да извършват ограничени действия
  • на


We’ll use supabase.auth.onAuthStateChange()да слушате тези събития и съответно да актуализирате приложението.


In the Layout.tsx file: 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;

Restrict Access on Protected Pages

Страници катоpoll detailsилиpoll management, you should also listen for authentication state changes to prevent unauthenticated users from accessing them.


Ето как изглежда в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;


Тези шаблони гарантират, че вашият потребителски интерфейс отразява текущото състояние на удостоверяване на потребителя и образува основата за проверките за упълномощаване, които ще добавим по-късно.userОбектът, когато се обаждаcheckPermissionФункция за определяне дали на потребителя е позволено да гласува или да управлява конкретно проучване.

Изграждане на функционалност на Polling App

С конфигурирането на Supabase и работата по удостоверяване, сега можем да изградим основната функционалност на приложението за гласуване.


  • Creating new polls
  • на
  • Fetching and displaying polls in real-time
  • на
  • Implementing a voting system
  • на


Това ни дава основното поведение на приложението, което скоро ще защитим с фини разрешения.

Създаване на нови анкети

Потребителите трябва да са влезли, за да създават проучвания.Всяко проучване включва въпрос, дата на изтичане и набор от опции.Ние също така записваме кой е създал проучването, за да можем по-късно да използваме тази връзка за контрол на достъпа.


Inside 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;

По-късно ще се обадим на Edge Function тук, за да присвоим ролята "креатор" в 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в Supabase. Също така създаваме абонамент в реално време, за да слушаме за промени вpollsтаблица, така че да можем да актуализираме потребителския интерфейс с най-новите данни от проучванията.За да разграничим между активни и минали проучвания, ние сравняваме датата на изтичане на срока на годност на всяко проучване с текущата дата.


Актуализиране наpages/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компонент, така че ако вписан потребител е създателят на проучването, иконите за редактиране и изтриване на проучването ще им се показват в проучването.


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;


Така че сега, на карта с проучвания, ако потребителят е създателят на проучването, ще им се показват икони за редактиране и изтриване на проучването.

Прилагане на системата за гласуване

Логиката на гласуване налага:

    на
  • Only one vote per user per poll
  • на
  • Създателите не могат да гласуват за собствените си проучвания
  • на
  • Votes are stored in the votes table
  • на
  • Резултатите се показват и актуализират в реално време
  • на


Нека да разберем как работи това вViewPoll.tsx component:


Fetch the Logged-In UserНуждаем се от идентификационния номер на текущия потребител, за да определим допустимостта за гласуване и да запишем гласа му.


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След като имаме потребителя, ние изтегляме:

    на
  • Самата анкета (включително опциите и преброяването на гласовете)
  • на
  • Whether this user has already voted
  • на


We also call these again later in real-time updates.


  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

Ако потребителят не е гласувал и му е позволено да гласува (по-късно ще добавим проверка за разрешение), ще вмъкнем гласа му.


  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 ResultsНие изчисляваме общия брой гласове и отчитаме до изтичане на срока на валидност.Това можете да използвате, за да покажете ленти за напредък или статистика.


  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ИзползванеPermit.ioиSupabase Edge Functionsза прилагане на тези правила.


Преди да направим това, нека първо разгледаме вида на разрешителния слой, който ще изпълним.

Релационен контрол на достъпа (Relationship-Based Access Control)

Supabase handles authentication and basic row-level permissions well, but it doesn’t support complex rules like:

  • Предотвратяване на потребителите да гласуват в собствените си проучвания
  • на
  • Assigning per-resource roles (like “creator” for a specific poll)
  • на
  • Управление на достъпа чрез външни политики


За да поддържаме тези видове разрешения, базирани на взаимоотношения, ще внедрим ReBAC с Permit.io.


Relationship-Based Access Control (ReBAC)Вместо да разчита само на роли или атрибути (като в RBAC или ABAC), ReBAC определя достъпа, като оценява как потребителят е свързан с ресурса, към който се опитва да получи достъп.

Релационен контрол на достъпа (ReBAC)


В този урок прилагаме ReBAC към приложение за гласуване:

    на
  • Потребителят, който е създал анкетата, е единственият, който може да я управлява (редактира/изтрива)
  • на
  • Потребителят не може да гласува за собствената си анкета
  • на
  • Други авторизирани потребители могат да гласуват веднъж на анкета


Чрез моделиране на тези взаимоотношения в Permit.io можем да дефинираме фини правила за достъп, които надхвърлят вградената защита на нивото на нивото на нивото (RLS) на Supabase.


За повече информация за ReBAC, проверетеДосиетата на Permit.io ReBAC. от

Access Control Design

For our Polling app, we will define:

    на
  • Един ресурс със специфични за ресурса действия: анкети: създаване, четене, изтриване, актуализиране.
  • на
  • Две роли за предоставяне на нива на разрешения въз основа на връзката на потребителя с ресурсите: автентичен: Може да изпълнява действия за създаване и четене в проучвания. Не може да изтрива или актуализира действия в проучвания. създател: Може да създава, чете, изтрива и актуализира действия в проучвания. Може да изпълнява действия за четене и създаване на гласове. Не може да използва създаване в собствените си проучвания.
  • на

Настройване на Permit.io

Нека да преминем през настройката на модела за разрешение в Разрешение.

    на
  • Създаване на нов проект in Permit.io
    • Name it something like supabase-polling
  • на
  • Определяне на ресурса на анкетите Отидете на раздела Политика → Ресурси Кликнете върху "Създаване на ресурс" Назовете анкетите и добавете действията: четене, създаване, актуализиране, изтриване
  • на
  • Активиране на ReBAC за ресурса В раздела "Опции за ReBAC" дефинирайте следните роли: автентичен създател Кликнете върху Запиши
  • на
Създаване на нов проект

Натиснете в раздела "Роли", за да видите ролите от ресурсите, които току-що създадохме.admin, editor,userТези неща не са необходими за този урок.


    на
  • Определяне на политики за достъп Отидете на раздела Правила Използвайте визуалната матрица, за да дефинирате: автентифициран може да чете и създава проучвания създателят може да чете, актуализира и изтрива проучвания (Необходимо) Можете да конфигурирате разрешения за гласуване като част от това или чрез втори ресурс, ако моделирате гласуването отделно
  • на
  • Добавяне на инстанции на ресурси Отидете в директория → Инстанции Добавяне на индивидуални идентификатори на проучвания като инстанции на ресурси (ще автоматизирате това по-късно, когато се създават нови проучвания) Присвояване на роли на потребители на проучване (например user123 е създател на проучване456)
  • на

Тази структура ни дава правомощието да пишем гъвкави правила за достъп и да ги прилагаме на потребител, на анкета.


Сега, когато сме завършили първоначалната настройка на таблото Разреши, нека я използваме в нашето приложение.Разрешителнокъм нашия проект Supabase чрез Edge Functions, който:

    на
  • Sync new users
  • на
  • Присвояване на ролите на създателя
  • на
  • Проверка на достъпа по заявка
  • на

Setting up Permit in the Polling Application

Разрешение предлага множество начини за интегриране с вашето приложение, но ние ще използваме Контейнер PDP за този урок. Трябва да хоствате контейнера онлайн, за да получите достъп до него в функциите на Supabase Edge. Можете да използвате услуги катоЖелезопътен транспорт.comСлед като го хоствате, запишете URL адреса за вашия контейнер.


Получете API клавиша Разреши, като щракнете върху "Проекти" в страничната лента на таблото Разреши, навигация към проекта, върху който работите, щракнете върху трите точки и изберете "Копирай API ключ".


Създаване на API за функции на Supabase Edge за разрешаване

Supabase Edge Functions are perfect for integrating third-party services like Permit.io. We’ll use them to enforce our ReBAC rules at runtime by checking whether users are allowed to perform specific actions on polls.

Create Functions in Supabase

Инициализирайте Supabase в проекта си и създайте три различни функции, като използватеsupabase functions newТова ще бъде отправна точка за вашите функции:

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

Това ще създаде аfunctionsФайлът вsupabaseСега, нека напишем кодовете за всяка крайна точка.

Изтегляне на отметка за изтегляне на отметка (syncUser.ts)

This function listens for Supabase’s SIGNED_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) on a specific poll.


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 dev сървър, за да тествате функциите на местно ниво:

npx supabase start 
npx supabase functions serve

След това можете да натиснете функциите си на:

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

Пример за:

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

Интегриране на проверките за разрешение в UI

Сега, когато създадохме нашата логика за разрешаване с Permit.io и я разкрихме чрез функциите на Supabase Edge, е време да наложим тези проверки в компонентите на приложението.


В този раздел ще актуализираме ключовите компоненти на потребителския интерфейс, за да се обадим на тези функции и условно да разрешаваме или блокираме действия на потребителя, като гласуване или управление на проучвания въз основа на проверки на разрешенията.

NewPoll.tsx: Присвояване на роля на създател след създаване на проучване

Съвпадение за: tsx

След като създадем анкета и я запазим в Supabase, ние наричамеupdateCreatorRoleФункцията е:

    на
  • Синхронизиране на новата анкета като ресурс в Permit.io
  • на
  • За да се предотврати възникването на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване на възникване
  • на

ViewPoll.tsxОграничаване на гласуването въз основа на разрешения

Преди да позволим на потребителя да гласува в проучване, ние наричамеcheckPermissionфункция, за да се провери дали те иматcreate permission on the 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>
)}

Тестване на интеграцията

След като сте интегрирани, трябва да видите следните поведения в приложението:

    на
  • Изключените потребители могат да разглеждат анкети, но не и да си взаимодействат
  • на
  • Автентифицираните потребители могат да гласуват за проучвания, които не са създали
  • на
  • Създателите не могат да гласуват за собствените си проучвания
  • на
  • Само създателите виждат опциите за редактиране/изтриване в анкетите си
  • на

Трябва да можете да видите промените на приложението, като отидете в браузъра. На началния екран потребителите могат да видят списъка с активни и минали проучвания, независимо дали са влезли или не. Въпреки това, когато кликнат върху проучване, те няма да могат да видят подробностите за проучването или да гласуват за него.


След като са влезли, потребителят може да прегледа подробностите за анкетата и да гласува за нея.Въпреки това, ако потребителят е създателят на анкетата, те няма да могат да гласуват за нея.Те ще видят съобщение, което показва, че не могат да гласуват за собствената си анкета.

Заключението

В това урок, ние разгледаме как да се прилагаSupabase authentication and authorizationВ реалния святNext.jsна приложението.

Започнахме със създаването наSupabase Authза вход и вход, създайте релационна схема с Row Level Security и добавете динамична логика за упълномощаване с помощта наReBACС помощта наSupabase Edge Functionsи аPolicy Decision Point (PDP), we enforced permission checks directly from the frontend.


Чрез комбиниранеSupabase AuthС гъвкав контрол на достъпа можем да:

    на
  • Автентифициране на потребителите чрез имейл и парола
  • на
  • Ограничаване на гласуването и управлението на анкетите до упълномощени потребители
  • на
  • Предотвратяване на създателите да гласуват за собствените си проучвания
  • на
  • Присвояване и оценка на потребителски роли въз основа на взаимоотношенията с данните
  • на


Тази настройка ви дава мащабируема основа за изграждане на приложения, които изискват както удостоверяване, така и фино разрешение.

По-нататъшно четене

    на
  • Permit.io Ребак Ръководство
  • на
  • Разрешение + Доставчици на удостоверяване
  • Разрешителни елементи: Вграден интерфейс за управление на роли
  • на
  • Филтриране на данни с разрешение
  • на
  • Проверка на дневниците
  • на

Имате въпроси?Присъединете се към насSlack community, където стотици разработчици изграждат и обсъждат разрешаването.

Слаба общност

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks