1,843 čitanja
1,843 čitanja

DIY Real-Time Polling aplikacija blokira pristup s Supabase i Permit.io

po Permit.io40m2025/04/16
Read on Terminal Reader

Predugo; Čitati

Cjeloviti vodič za izgradnju sigurne aplikacije za anketiranje u stvarnom vremenu s autentifikacijom, dozvolama po korisniku i dinamičkim provjerama politika – koristeći Supabase + Permit.io.
featured image - DIY Real-Time Polling aplikacija blokira pristup s Supabase i Permit.io
Permit.io HackerNoon profile picture
0-item


zaGabriel L. Manor


Supabase omogućuje jednostavno dodavanje autentifikacije vašoj aplikaciji uz ugrađenu podršku za e-poštu, OAuth i čarobne veze.


Supabase nudi odličan backend s ugrađenim auth i Row Level Security (RLS), upravljanjemfine-grained permissionsPogotovo one koje se temelje narelationships between users and dataDaleko je od lakoće.


Možda želite ograničiti radnje kao što su uređivanje ili brisanje podataka vlasnicima resursa, spriječiti korisnike da glasuju o vlastitom sadržaju ili izvršiti različite dozvole za različite uloge korisnika.


Ovaj tutorial govori o tome kako to učinitiSupabase authentication and authorizationU ANext.jsAplikacija za.

Mi ćemo početi sSupabase Authza login i upravljanje sjednicama, a zatim dodajteauthorization ruleskorištenjeKontrola pristupa na temelju odnosa (ReBAC)Primjenjivao se krozSupabase Edge Functionsi alocal Policy Decision Point (PDP). u


Na kraju ćete imati aplikaciju za suradnju u stvarnom vremenu koja podržava i javne i zaštićene radnje - i fleksibilan sustav ovlaštenja koji se može razviti kako vaša aplikacija raste.

Što gradimo

U ovom priručniku izgradit ćemo aplikaciju za glasanje u realnom vremenu pomoćuSupabaseiNext.jsTo pokazuje kako autentifikaciju i autorizaciju u akciji.


Aplikacija omogućuje korisnicima da kreiraju ankete, glasuju za druge i upravljaju samo vlastitim sadržajem.Supabase Authza login/signup i kako izvršitiauthorization policiesTko može glasovati, uređivati ili brisati.


Upotrijebit ćemo glavne značajke Supabase-a -Auth,Postgres,RLS,RealtimeiEdge Functions—combined with a Relationship-Based Access Control (ReBAC)Model za provedbu pravila pristupa po korisniku i po resursima.

Tehnologija Stack

    Svijet
  • Supabase – Backend-as-a-service za baze podataka, autentifikaciju, funkcije u realnom vremenu i Edge
  • Svijet
  • Next.js – Frontend framework za izgradnju aplikacije UI i API putova
  • Svijet
  • Permit.io – (za ReBAC) definirati i ocijeniti logiku ovlaštenja putem PDP-a
  • Svijet
  • Supabase CLI – za upravljanje i uvođenje Edge funkcija na lokalnoj razini i u proizvodnji
  • Svijet
pretpostavkaSljedeći.jsdopuštenjeSljedeći članakCLI

Preduvjeti

    Svijet
  • Node.js je instaliran
  • Svijet
  • Baza računa
  • Svijet
  • Dozvola.io račun
  • Svijet
  • Saznajte više o React/Next.js
  • Svijet
  • Početak projekta Repo
  • Svijet

What Can This App Do?

Demo aplikacija je platforma za anketiranje u stvarnom vremenu izgrađena uz Next.js i Supabase, gdje korisnici mogu kreirati ankete i glasati za druge.


    Svijet
  • Svaki korisnik (autentificiran ili ne) može vidjeti popis javnih anketa
  • Svijet
  • Samo autentični korisnici mogu kreirati ankete i glasati
  • Korisnik ne može glasati na anketi koju je stvorio
  • Svijet
  • Samo kreator ankete može ga urediti ili izbrisati
  • Svijet

Tutorial Pregled

Slijedit ćemo ove opće korake:


    Svijet
  1. Uspostavljanje Supabase projekta, sheme, auth i RLS
  2. Svijet
  3. Izgradite osnovne funkcije aplikacije kao što su kreiranje anketa i glasovanje
  4. Svijet
  5. Pravila za autorizaciju modela definiraju uloge i pravila u Permit.io
  6. Svijet
  7. Stvaranje funkcija Supabase Edge za sinhronizaciju korisnika, dodjelu uloga i provjeru dozvola
  8. Svijet
  9. Provedba politika u prednjem dijelu aplikacije pomoću tih funkcija ruba
  10. Svijet


Hajde da počnemo -

Setting up Supabase in the Project

Uspostavljanje baze podataka u projektu

Opcionalno: Klonirajte predložak Starter

Već sam stvorio aPočetak hramovaOn jeGitHubsa svim kodom koji trebate započeti kako bismo se mogli usredotočiti na provedbu Supabase i Permit.io.


You can clone the project by running the following command:


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


Nakon što ste klonirali projekt, idite u projektni direktorij i instalirajte ovisnosti:


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

Creating a new Project in Supabase

Kako započeti:

    Svijet
  • Idite na https://supabase.com i prijavite se ili kreirajte račun.
  • Svijet
  • Kliknite na "Novi projekt" i unesite ime projekta, lozinku i regiju.
  • Svijet
  • Nakon što ste ga stvorili, idite na Project Settings → API i zabilježite svoj Project URL i Anon Key – potrebni su vam kasnije.
  • Svijet

Postavljanje autentifikacije i baze podataka u Supabase-u

Upotrijebit ćemo ugrađenu e-poštu / lozinku Auth:

    Svijet
  • U bočnoj traci, idite na Authentication → Providers
  • Omogućite pružatelja e-pošte
  • Svijet
  • (Opcionalno) Deaktivirajte potvrdu e-pošte za testiranje, ali je držite uključenom za proizvodnju
  • Svijet

Stvaranje sheme baze podataka

Ova aplikacija koristi tri glavne tablice:polls,optionsivotesKoristite gaSQL Editoru upravljačkoj ploči Supabase i pokrenite sljedeće:


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

Omogućavanje sigurnosti na razini linije (RLS)

OmogućitiRLSza svaku tablicu i definirati politike:

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


Za korištenje funkcija u realnom vremenu baze:

    Svijet
  • U bočnoj traci, idite na Urednik stola
  • Svijet
  • Za svaku od tri tablice (polls, opcije, glasovi): Kliknite na tri točke → Uredi tablicu Toggle "Omogući u stvarnom vremenu" Sačuvaj promjene
  • Svijet

Implementacija Supabase e-pošte autentifikacije u aplikaciji

U ovoj demo aplikaciji, svatko može pregledati popis anketa dostupnih na aplikaciji, aktivnih i isteklih. Da biste pregledali pojedinosti ankete, upravljali ili glasovali o bilo kojoj anketi, korisnik mora biti prijavljen. Mi ćemo koristiti e-mail i lozinku kao sredstvo autentifikacije za ovaj projekt..env.local: u

NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key


Ažurirajte svoju komponentu za prijavu kako biste se bavili prijavom i prijavom putem e-pošte / lozinke:


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;


Ovdje, mi koristimo SupabasesignInWithPasswordnačin prijavljivanja u korisnika isignUpnačin prijavljivanja novog korisnika s njihovom e-poštom i lozinkom. također pohranjujemo ime korisnika uuser_namepolja u korisničkim metapodatcima.


Također možete koristitisupabase.auth.signOut()kako bi se korisnici prijavili i preusmjerili:


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;


Ovdje koristimosignOutMetoda iz Supabase za izlazak korisnika i preusmjeriti ih na početnu stranicu.

Slušanje za promjene u stanju autentifikacije korisnika

Slušanje o promjenama u statusu autentifikacije korisnika omogućuje nam ažuriranje korisničkog interfejsa na temelju statusa autentifikacije korisnika.


  • Show/hide UI elements like login/logout buttons
  • Svijet
  • Uvjetno ograničiti pristup zaštićenim stranicama (kao što je glasovanje ili upravljanje anketama)
  • Svijet
  • Ensure only authenticated users can perform restricted actions
  • Svijet


Upotrijebit ćemosupabase.auth.onAuthStateChange()slušati te događaje i ažurirati aplikaciju u skladu s tim.


In theSvijetLayout.tsxSvijetfile: 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;

Ograničiti pristup zaštićenim stranicama

On pages like poll detailsilipoll management, također biste trebali slušati promjene stanja autentifikacije kako biste spriječili neovjerene korisnike da im pristupe.


Evo kako to izgleda upages/polls/[id].tsx: u

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;


And a similar pattern applies in pages/polls/manage.tsx, gdje korisnici trebaju vidjeti vlastite ankete samo ako su prijavljeni:


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;


Ovi obrasci osiguravaju da vaš UI odražava trenutni status autentifikacije korisnika i predstavlja osnovu za provjere ovlaštenja koje ćemo kasnije dodati.userObjekt kada se zovecheckPermissionEdge Funkcija za određivanje je li korisniku dopušteno glasovati ili upravljati određenom anketom.

Izgradnja funkcionalnosti aplikacije Polling

With Supabase configured and authentication working, we can now build the core functionality of the polling app. In this section, we’ll cover:


  • Stvaranje novih anketa
  • Svijet
  • Preuzimanje i prikazivanje anketa u realnom vremenu
  • Svijet
  • Uvođenje sustava glasovanja
  • Svijet


This gives us the basic app behavior that we’ll soon protect with fine-grained permissions.

Creating New Polls

Korisnici moraju biti prijavljeni kako bi mogli kreirati ankete.Svako istraživanje uključuje pitanje, datum isteka i skup opcija.Mi također bilježe tko je stvorio istraživanje kako bismo kasnije mogli koristiti taj odnos za kontrolu pristupa.


UnutarNewPoll.tsx, preuzmite autentificiranog korisnika i upotrijebite Supabase za umetanje ankete i njezinih opcija:


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;

We’ll later call an Edge Function here to assign the “creator” role in Permit.io.

Fetching and Displaying Polls

Polls are divided into active(Još nije istekao) ipastMožete ih preuzeti pomoću upita Supabase filtriranih trenutnim vremenskim žigom i postaviti pretplate u realnom vremenu kako biste odmah odražavali promjene.


Primjer izpages/index.tsx: u


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

Pretraživanje i upravljanje korisničkim anketama

Ovdje prikupljamo aktivne i prethodne ankete izpollsTakođer postavljamo pretplatu u realnom vremenu kako bismo slušali promjene upollsDa bismo razlikovali između aktivnih i prošlih anketa, uspoređujemo datum isteka svakog anketa s trenutnim datumom.


ažuriranje napages/manage.tsxstranica za preuzimanje i prikazivanje samo anketa koje je stvorio korisnik:


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;


Here, we only fetch polls created by the user and listen for real-time updates in the pollstablicu tako da je UI ažuriran s najnovijim podacima o anketama.


Also, update the PollCardkomponenta tako da ako je prijavljeni korisnik kreator ankete, ikone za uređivanje i brisanje ankete će im se prikazati na anketi.


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;


Dakle, sada, na kartici ankete, ako je prijavljeni korisnik kreator ankete, prikazat će im se ikone za uređivanje i brisanje ankete.

Uvođenje sustava glasovanja

Logika glasovanja primjenjuje:

    Svijet
  • Samo jedan glas po korisniku po anketi
  • Kreatori ne mogu glasati na vlastitim anketama
  • Svijet
  • Glasovi se pohranjuju u tablicu za glasovanje
  • Svijet
  • Rezultati se prikazuju i ažuriraju u realnom vremenu
  • Svijet


Let’s break down how this works in the ViewPoll.tsxkomponentima :


Fetch the Logged-In UserPotreban nam je ID trenutačnog korisnika kako bismo odredili prihvatljivost za glasovanje i zabilježili njihov glas.


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 StatusNakon što imamo korisnika, prikupljamo:

  • Sam izbor (uključujući opcije i brojanje glasova)
  • Da li je ovaj korisnik već glasao
  • Svijet


Također ćemo ih nazvati kasnije u ažuriranjima u realnom vremenu.


  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

Prihvaćamo promjene uvotesKada se glasa novo, prikupljamo ažurirane podatke o anketama i status glasa.


    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

Ako korisnik nije glasao i dopušteno je glasovati (dodat ćemo provjeru dopuštenja kasnije), umetnemo njegov glas.


  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 ResultsIzračunamo ukupni broj glasova i odbrojavanje do vremena isteka, a zatim ga možete koristiti za prikaz postupka ili statistike.


  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;


With this setup in place, your voting system is fully functional. But right now, anyone logged in could technically try to vote—even on their own poll. Next, we’ll add authorization checkskorištenjePermit.io and Supabase Edge Functionsza provođenje tih pravila.


Prije nego što to učinimo, prvo pogledajmo vrstu sloja ovlaštenja koji ćemo implementirati.

ReBAC (Relationship-Based Access Control) – kontrola pristupa na temelju odnosa

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

    Svijet
  • Kako spriječiti korisnike da glasaju na vlastitim anketama
  • Assigning per-resource roles (like “creator” for a specific poll)
  • Managing access via external policies
  • Svijet


Da bismo podržali ove vrste dozvola zasnovanih na odnosima, implementirat ćemo ReBAC s Permit.io.


Relationship-Based Access Control (ReBAC)Umjesto da se oslanja isključivo na uloge ili atribute (kao u RBAC-u ili ABAC-u), ReBAC određuje pristup procjenjujući kako je korisnik povezan s resursom koji pokušavaju pristupiti.

Kontrola pristupa na temelju odnosa (ReBAC)


In this tutorial, we apply ReBAC to a polling app:

    Svijet
  • Korisnik koji je stvorio anketu je jedini koji može upravljati (uredi / izbriši)
  • Svijet
  • Korisnik ne može glasati na vlastitoj anketi
  • Other authenticated users can vote once per poll


Modeliranjem tih odnosa u Permit.io-u možemo definirati pravila pristupa koja nadilaze ugrađenu sigurnost na razini linije (RLS) tvrtke Supabase.


Za više informacija o ReBAC-u provjeritePermit.io’s ReBAC dokazi. u

Dizajn kontrole pristupa

For our Polling app, we will define:

    Svijet
  • Jedan resurs s akcijama specifičnim za resurse: ankete: stvaranje, čitanje, brisanje, ažuriranje.
  • Svijet
  • 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.
  • Svijet

Preuzimanje dozvola.io

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

    Svijet
  • Stvoriti novi projekt u Permit.io Nazovite ga nešto poput supabase-polling
  • Definicija resursa za ankete Idite na karticu Politika → resursi Kliknite na "Stvoriti resurs" Nazovite to ankete i dodajte akcije: čitati, stvoriti, ažurirati, izbrisati
  • Svijet
  • Omogućiti ReBAC za resurs U odjeljku "ReBAC opcije" definirajte sljedeće uloge: autentificirani kreator Kliknite Save
Create a new project

Preuzmite uloge koje smo upravo stvorili.Upozorite da je dopuštenje stvorilo podrazumijevane uloge (admin,editor, userTo je nepotrebno za ovaj tutorial.


    Svijet
  • Define access policies

    • Go to the Policy → Policies tab
    • Use the visual matrix to define:
      • authenticated can read and create polls

      • creator can read, update, and delete polls

      • (Optional) You can configure vote permissions as part of this or via a second resource if you model votes separately



  • Svijet
  • Dodaj instance resursa Idi u Direktorij → Instance Dodaj pojedinačne ID-ove anketa kao instance resursa (to ćete automatizirati kasnije kada se kreiraju nove ankete) Dodijelite uloge korisnicima po anketi (npr. user123 je kreator ankete456)
  • Svijet

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


Now that we have completed the initial setup on the Permit dashboard, let's use it in our application. Next, we’ll connect dopuštenjena naš projekt Supabase putem Edge Functions koji:

    Svijet
  • Sinkronizirati nove korisnike
  • Svijet
  • Dodjeljivanje uloga kreatora
  • Svijet
  • Provjerite pristup na zahtjev
  • Svijet

Setting up Permit in the Polling Application

Dozvola nudi nekoliko načina za integraciju s vašom aplikacijom, ali mi ćemo koristiti Container PDP za ovaj tutorial. Morate gostovati kontejner online da biste ga pristupili u funkcijama Supabase Edge.Željeznica.com. Once you have hosted it, save the url for your container.


Pronađite ključ API-ja za dopuštenje klikom na "Projekti" u bočnoj traci nadzorne ploče za dopuštenje, navigacijom prema projektu na kojem radite, klikom na tri točke i odabirom "Kopiraj ključ API-ja".


Creating Supabase Edge Function APIs for Authorization

Supabase Edge FunctionsOni su savršeni za integraciju usluga trećih strana kao što je Permit.io. Mi ćemo ih koristiti za provedbu naših pravila ReBAC-a u tijeku provjeravanjem je li korisnicima dopušteno obavljati određene radnje na anketama.

Create Functions in Supabase

Inicijalizirajte Supabase u projektu i kreirajte tri različite funkcije pomoćusupabase functions newOvo će biti početna točka za vaše funkcije:

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

To će stvoriti afunctionsBrošura usupabaseSada, hajde da napišemo kode za svaku krajnju točku.

Sljedeći Članak Sljedeći Članak Sinhroniziranje korisnika na Permit.io na Signup (syncUser.ts) i

Ova funkcija sluša za Supabase'sSIGNED_UP auth event. When a new user signs up, we sync their identity to Permit.io and assign them the default authenticatedUloga je.


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

Prikazivanje uloge Stvoritelja (updateCreatorRole.ts) i

Nakon što korisnik stvori anketu, ova se funkcija poziva na:

    Svijet
  • Sinhronizirajte anketu kao novu instanci resursa Permit.io
  • Assign the user the creator role for that 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, pollId } = await req.json();
    
        // Validate input parameters
        if (!userId || !pollId) {
          return new Response(
            JSON.stringify({ error: "Missing required parameters." }),
            { status: 400, headers: { "Content-Type": "application/json" } },
          );
        }
    
        // Sync the resource (poll) to Permit.io
        await permit.api.syncResource({
          type: "polls",
          key: pollId,
          tenant: "default",
          attributes: {
            createdBy: userId
          }
        });
    
        // Assign the creator role to the user for this specific poll
        await permit.api.assignRole({
          role: "creator",
          tenant: "default",
          user: userId,
          resource: {
            type: "polls",
            key: pollId,
          }
        });
    
        return new Response(
          JSON.stringify({ 
            message: "Creator role assigned successfully",
            success: true 
          }),
          { status: 200, headers: corsHeaders },
        );
      } catch (error) {
        console.error("Error assigning creator role: ", error);
    
        return new Response(
          JSON.stringify({
            message: "Error occurred while assigning creator role.",
            error: error
          }),
          { status: 500, headers: { "Content-Type": "application/json" } },
        );
      }
    });
    


  • Svijet

Sljedeći Članak Pregled dopuštenja (checkPermission.ts) i

Ova funkcija djeluje kao čuvar vrata – provjerava je li korisniku dopušteno izvršiti određenu akciju (create,read, update,deleteNa posebnoj anketi.


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

Local Testing

Počnite svoj Supabase dev server kako biste testirali funkcije lokalno:

npx supabase start 
npx supabase functions serve

Tada možete dodirnuti svoje funkcije na:

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

Primjer :

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

Integracija provjera ovlaštenja u 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.


U ovom odjeljku ažurirat ćemo ključne komponente korisničkog interfejsa kako bismo pozvali te funkcije i uvjetno omogućili ili blokirali korisničke akcije kao što su glasovanje ili upravljanje anketama na temelju provjera dozvola.

NewPoll.tsxDodijeliti ulogu kreatora nakon kreiranja ankete

Sljedeći članakPoll.tsx

Nakon što kreiramo anketu i sačuvamo je u Supabase, zovemoupdateCreatorRoleFunkcija je:

    Svijet
  • Sinhronizirajte novu anketu kao resurs u Permit.io
  • Svijet
  • Korisnik koji je stvorio funkciju za postavljanje postavke za trenutni korisnik je stvorio funkciju za postavljanje postavke za trenutni korisnik (Error.Trim() &&options.filter(opt => opt.trim()).Length < 2) { setErrorMessage("ExpiryDate).to ISOString("Please provide a question and at least two options."); return; try { // Create the error const: user_metadata?.Anuser_name, await error: pollError() &&options.filter(opt => opt.trim()) .insert{ question, expires_at: Date new(ExpiryDate).to ISOString), created_by user_error_role(Error_name, error_response: user_metadata?.Anuser_
  • Svijet

ViewPoll.tsxOgraničenje glasovanja na temelju dozvola

Prije nego što dopustimo korisniku da glasa na anketi, zovemocheckPermissionfunkcije kako bi se provjerilo da imajucreateDopuštenje zavotesEvo kako se primjenjuje pravilo:“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

Također ograničavamo radnje upravljanja anketama (uređivanje i brisanje) provjeravanjem ima li korisnikupdateilideleteOdobrenje za tu anketu.


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:


Zamjena za:

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


sa :

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

Testiranje integracije

Nakon integracije, trebali biste vidjeti sljedeće ponašanje u aplikaciji:

    Svijet
  • Isključeni korisnici mogu vidjeti ankete, ali ne i komunicirati
  • Svijet
  • Autentificirani korisnici mogu glasati na anketama koje nisu stvorili
  • Svijet
  • Kreatori ne mogu glasati na vlastitim anketama
  • Svijet
  • Samo kreatori vide opcije za uređivanje / brisanje na svojim anketama
  • Svijet

You should be able to see the application's changes by going to the browser. On the home screen, users can view the list of active and past polls, whether they are logged in or not. However, when they click on a poll, they will not be able to view the poll details or vote on it. Instead, they will be prompted to log in.


Once logged in, the user can view the details of the poll and vote on it. However, if the user is the creator of the poll, they will not be able to vote on it. They will see a message indicating that they cannot vote on their own poll. They will also be allowed to manage any poll that they create.

Zaključak

In this tutorial, we explored how to implement Supabase authentication and authorizationU stvarnom svijetuNext.jsAplikacija za.

Počeli smo s postavljanjemSupabase Authza prijavu i prijavu, stvorio relacijsku shemu s sigurnosnim sustavom razine rublja i dodao dinamičku logiku ovlaštenja pomoćuReBACUz pomoćSupabase Edge Functionsi aPolicy Decision Point (PDP), izvršili smo provjere dozvola izravno s frontenda.


KombinirajućiSupabase AuthS fleksibilnom kontrolom pristupa mogli smo:

    Svijet
  • Autentificiranje korisnika putem e-pošte i lozinke
  • Svijet
  • Ograničiti glasovanje i upravljanje anketama na ovlaštene korisnike
  • Svijet
  • Sljedeći Članak Sprječavanje kreatora da glasaju na vlastitim anketama
  • Svijet
  • Dodjeljivanje i ocjenjivanje uloga korisnika na temelju odnosa s podacima
  • Svijet


Ova postavka pruža vam skalabilnu osnovu za izgradnju aplikacija koje zahtijevaju autentifikaciju i autorizaciju s finim zrnom.

daljnje čitanje

    Svijet
  • Permit.io ReBAC vodič
  • Svijet
  • Dozvola + pružatelji autentikacije
  • Svijet
  • Elementi dopuštenja: ugrađeni UI za upravljanje ulogama
  • Svijet
  • Filtriranje podataka s dopuštenjem
  • Svijet
  • Revizorski logovi
  • Svijet

Imate pitanja? pridružite nam seSlack zajednica, gdje stotine programera grade i raspravljaju o autorizaciji.

Slack zajednica

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks