bởiGabriel L. Manor
Supabase giúp dễ dàng thêm xác thực vào ứng dụng của bạn với hỗ trợ tích hợp cho email, OAuth và liên kết ma thuật.Nhưng trong khi Supabase Auth xử lý người dùng của bạn, bạn thường cần một lớp ủy quyền cũng vậy.
Supabase cung cấp một backend tuyệt vời với built-in auth và Row Level Security (RLS), quản lýfine-grained permissionsĐặc biệt là những người dựa trênrelationships between users and data- Quá xa là dễ dàng.
Bạn có thể muốn hạn chế các hành động như chỉnh sửa hoặc xóa dữ liệu cho chủ sở hữu tài nguyên, ngăn người dùng bỏ phiếu về nội dung của riêng họ, hoặc thực thi các quyền khác nhau cho vai trò người dùng khác nhau.
Hướng dẫn này đi qua làm thế nào để thực hiệnSupabase authentication and authorizationtrong aNext.jsỨng dụng
Chúng ta sẽ bắt đầu vớiSupabase Authcho login và quản lý phiên, sau đó thêmauthorization rules using Kiểm soát truy cập dựa trên mối quan hệ (ReBAC), áp dụng thông quaSupabase Edge Functionsvà alocal Policy Decision Point (PDP)của
Cuối cùng, bạn sẽ có một ứng dụng khảo sát cộng tác thời gian thực hỗ trợ cả hành động công khai và được bảo vệ - và một hệ thống ủy quyền linh hoạt mà bạn có thể phát triển khi ứng dụng của bạn phát triển.
Những gì chúng tôi đang xây
Trong hướng dẫn này, chúng tôi sẽ xây dựng một ứng dụng thăm dò thời gian thực bằng cách sử dụngSupabasevàNext.jsĐiều này cho thấy cả xác thực và ủy quyền trong hành động.
Ứng dụng cho phép người dùng tạo các cuộc thăm dò, bỏ phiếu cho người khác và chỉ quản lý nội dung của riêng họ.Supabase Authcho login / signup và làm thế nào để thực thiauthorization policiesđiều khiển người có thể bỏ phiếu, chỉnh sửa hoặc xóa.
Chúng tôi sẽ sử dụng các tính năng cốt lõi của Supabase -Auth,Postgres,RLS,RealtimevàEdge Functions- Kết hợp với aRelationship-Based Access Control (ReBAC)mô hình để thực thi các quy tắc truy cập per user và per resource.
Công nghệ Stack
- Thì
- Supabase – Backend-as-a-service cho cơ sở dữ liệu, xác thực, thời gian thực và các chức năng cạnh Thì
- Next.js – Framework Frontend để xây dựng ứng dụng UI và API routes Thì
- Permit.io – (đối với ReBAC) để xác định và đánh giá logic ủy quyền thông qua PDP Thì
- Supabase CLI – Để quản lý và triển khai các chức năng Edge tại địa phương và trong sản xuất Thì
Điều kiện
- Thì
- Node.js đã cài đặt Thì
- Tài khoản Supabase Thì
- Permit.io account Thì
- Giới thiệu về React/Next.js Thì
- Dự án Repo Thì
App này có thể làm gì?
Ứng dụng demo là một nền tảng thăm dò thời gian thực được xây dựng với Next.js và Supabase, nơi người dùng có thể tạo các cuộc thăm dò và bỏ phiếu cho người khác.
- Thì
- Bất kỳ người dùng nào (được xác thực hay không) có thể xem danh sách các cuộc thăm dò công khai Thì
- Chỉ người dùng được xác thực mới có thể tạo các cuộc thăm dò và bỏ phiếu Thì
- Một người dùng không thể bỏ phiếu trên một cuộc thăm dò mà họ đã tạo Thì
- Only the creator of a poll can edit or delete it Thì
Tutorial tổng quan
Chúng tôi sẽ làm theo các bước chung này:
- Thì
- Thiết lập dự án Supabase, Schema, Auth và RLS Thì
- Xây dựng các tính năng cốt lõi của ứng dụng như tạo cuộc thăm dò và bỏ phiếu Thì
- Quy tắc ủy quyền mô hình xác định vai trò và quy tắc trong Permit.io Thì
- Tạo chức năng Supabase Edge để đồng bộ người dùng, gán vai trò và kiểm tra quyền Thì
- Thực thi các chính sách trong frontend ứng dụng bằng cách sử dụng các hàm Edge đó Thì
Hãy bắt đầu -
Setting up Supabase in the Project
Thiết lập Supabase trong dự ánTùy chọn: Clone the Starter Template
Tôi đã tạo ra mộtBắt đầu templateCóGitHubvới tất cả các mã bạn cần để bắt đầu để chúng tôi có thể tập trung vào việc thực hiện Supabase và Permit.io.
Bạn có thể clone dự án bằng cách chạy lệnh sau:
git clone <https://github.com/permitio/supabase-fine-grained-authorization>
Sau khi bạn đã nhân bản dự án, hãy điều hướng đến thư mục dự án và cài đặt các phụ thuộc:
cd realtime-polling-app-nextjs-supabase-permitio
npm install
Tạo một dự án mới trong Supabase
Để bắt đầu:
- Thì
- Đi đến https://supabase.com và đăng nhập hoặc tạo tài khoản. Thì
- Nhấp vào "New Project" và điền tên dự án, mật khẩu và khu vực của bạn. Thì
- Sau khi tạo, hãy đi đến Thiết đặt dự án → API và lưu ý URL dự án của bạn và Anon Key – bạn sẽ cần chúng sau. Thì
Thiết lập xác thực và cơ sở dữ liệu trong Supabase
Chúng tôi sẽ sử dụng email/password auth tích hợp của Supabase:
- Thì
- Trong thanh bên, đi đến xác thực → Nhà cung cấp Thì
- Cho phép nhà cung cấp email Thì
- (Tùy chọn) Tắt email xác nhận để kiểm tra, nhưng giữ cho nó được bật cho sản xuất Thì
Tạo Database Schema
Ứng dụng này sử dụng ba bảng chính:polls
,options
vàvotes
Sử dụng theSQL Editortrong bảng điều khiển Supabase và chạy như sau:
-- 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)
);
Bảo mật cấp hàng (Row Level Security - RLS)
cho phépRLSĐối với mỗi bảng và xác định chính sách:
-- 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()
)
);
Để sử dụng các tính năng thời gian thực của Supabase:
- Thì
- Trong thanh bên, đi đến Table Editor Thì
- Đối với mỗi trong ba bảng (các cuộc thăm dò, tùy chọn, phiếu bầu): Nhấp vào ba điểm → Sửa bảng Toggle "Enable Realtime" Lưu các thay đổi Thì
Thực hiện Supabase Email Authentication trong ứng dụng
Trong ứng dụng demo này, bất cứ ai cũng có thể xem danh sách các cuộc thăm dò có sẵn trên ứng dụng, cả hoạt động và hết hạn. Để xem chi tiết của một cuộc thăm dò, quản lý hoặc bỏ phiếu cho bất kỳ cuộc thăm dò nào, người dùng phải đăng nhập. Chúng tôi sẽ sử dụng email và mật khẩu như phương tiện xác thực cho dự án này..env.local
:
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
Cập nhật thành phần đăng nhập của bạn để xử lý cả đăng nhập và đăng nhập qua email / mật khẩu:
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;
Ở đây, chúng tôi đang sử dụng SupabasesignInWithPassword
phương pháp để đăng nhập vào một người dùng vàsignUp
phương pháp để đăng ký một người dùng mới với email và mật khẩu của họ. chúng tôi cũng lưu trữ tên người dùng tronguser_name
trường trong metadata của người dùng.
Bạn cũng có thể sử dụngsupabase.auth.signOut()
Để đăng xuất người dùng và chuyển hướng họ:
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;
Ở đây, chúng tôi đang sử dụng cácsignOut
phương pháp từ Supabase để đăng xuất người dùng và chuyển hướng họ đến trang chủ.
Lắng nghe thay đổi trạng thái xác thực của người dùng
Listening for changes in the user's authentication state allows us to update the UI based on the user's authentication status. This allows you to:
- Thì
- Hiển thị / ẩn các yếu tố UI như nút login / logout Thì
- Conditionally restrict access to protected pages (like voting or managing polls) Thì
- Đảm bảo chỉ người dùng được xác thực có thể thực hiện các hành động bị hạn chế Thì
Chúng ta sẽ sử dụngsupabase.auth.onAuthStateChange()
để lắng nghe các sự kiện này và cập nhật ứng dụng phù hợp.
In the Layout.tsx
Thì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;
Giới hạn truy cập vào các trang được bảo vệ
Các trang nhưpoll detailshoặcpoll management, bạn cũng nên lắng nghe thay đổi trạng thái xác thực để ngăn người dùng không xác thực truy cập chúng.
Here’s how it looks in 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;
Và một mô hình tương tự áp dụng trongpages/polls/manage.tsx
, where users should only see their own polls if logged in:
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;
Những mẫu này đảm bảo UI của bạn phản ánh trạng thái xác thực hiện tại của người dùng và tạo thành cơ sở cho các kiểm tra ủy quyền mà chúng tôi sẽ thêm sau.user
Đối tượng khi gọicheckPermission
Edge Chức năng để xác định xem một người dùng có được phép bỏ phiếu hay quản lý một cuộc thăm dò cụ thể.
Xây dựng chức năng của ứng dụng Polling
Với Supabase được cấu hình và xác thực làm việc, bây giờ chúng tôi có thể xây dựng chức năng cốt lõi của ứng dụng thăm dò.
- Thì
- Tạo các polls mới
- Fetching and displaying polls in real-time
- Thực hiện một hệ thống bỏ phiếu Thì
Điều này cung cấp cho chúng tôi hành vi ứng dụng cơ bản mà chúng tôi sẽ sớm bảo vệ với các quyền tinh tế.
Tạo Polls mới
Người dùng phải đăng nhập để tạo các cuộc thăm dò. Mỗi cuộc thăm dò bao gồm một câu hỏi, ngày hết hạn và một tập hợp các tùy chọn. Chúng tôi cũng ghi lại ai đã tạo ra cuộc thăm dò để chúng tôi có thể sau này sử dụng mối quan hệ đó để kiểm soát truy cập.
bên trongNewPoll.tsx
, fetch the authenticated user, and use Supabase to insert the poll and its options:
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;
Sau đó chúng tôi sẽ gọi một hàm Edge ở đây để gán vai trò “creator” trong Permit.io.
Fetching và Displaying Polls
Polls are divided into active(Vẫn chưa hết hạn) vàpastBạn có thể lấy chúng bằng cách sử dụng các truy vấn Supabase được lọc theo dấu thời gian hiện tại, và thiết lập đăng ký thời gian thực để phản ánh các thay đổi ngay lập tức.
Ví dụ từ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 (
...
);
}
Xem và quản lý các cuộc thăm dò người dùng
Here, we are fetching active and past polls from the polls
bảng trong Supabase.Chúng tôi cũng đang thiết lập đăng ký thời gian thực để nghe về những thay đổi trongpolls
bảng để chúng tôi có thể cập nhật UI với dữ liệu thăm dò mới nhất. Để phân biệt giữa các cuộc thăm dò hiện tại và trước đây, chúng tôi đang so sánh ngày hết hạn của mỗi cuộc thăm dò với ngày hiện tại.
Cập nhật Thepages/manage.tsx
trang để lấy và hiển thị chỉ các cuộc thăm dò được tạo bởi người dùng:
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;
Ở đây, chúng tôi chỉ thu thập các cuộc thăm dò được tạo bởi người dùng và lắng nghe các bản cập nhật thời gian thực trongpolls
bảng để UI được cập nhật với dữ liệu khảo sát mới nhất.
Ngoài ra, cập nhật cácPollCard
thành phần để nếu người dùng đã đăng nhập là người tạo khảo sát, các biểu tượng để chỉnh sửa và xóa khảo sát sẽ được hiển thị cho họ trong khảo sát.
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;
Vì vậy, bây giờ, trên thẻ khảo sát, nếu người dùng đã đăng nhập là người tạo khảo sát, các biểu tượng để chỉnh sửa và xóa khảo sát sẽ được hiển thị cho họ.
Thực hiện hệ thống bỏ phiếu Poll
Logic bỏ phiếu áp dụng:
- Thì
- Only one vote per user per poll
- Người sáng tạo không thể bỏ phiếu trên các cuộc thăm dò của riêng họ Thì
- Các phiếu bầu được lưu trữ trong bảng bỏ phiếu Thì
- Kết quả được hiển thị và cập nhật trong thời gian thực Thì
Chúng ta hãy chia nhỏ cách điều này hoạt động trongViewPoll.tsx
Thành phần :
Fetch the Logged-In UserChúng tôi cần ID của người dùng hiện tại để xác định quyền bỏ phiếu và ghi lại phiếu bầu của họ.
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 StatusMột khi chúng tôi có người dùng, chúng tôi thu thập:
- Thì
- Bản thân cuộc thăm dò (bao gồm các lựa chọn và đếm phiếu)
- Nếu người dùng này đã bỏ phiếu Thì
Chúng tôi cũng gọi chúng trở lại sau này trong các bản cập nhật thời gian thực.
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
Chúng tôi đồng ý với những thay đổi trongvotes
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
Nếu người dùng không bỏ phiếu và được phép bỏ phiếu (chúng tôi sẽ thêm một kiểm tra quyền sau), chúng tôi chèn phiếu của họ.
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 ResultsChúng tôi tính tổng số phiếu bầu và đếm ngược đến thời gian hết hạn, sau đó bạn có thể sử dụng điều này để hiển thị thanh tiến độ hoặc số liệu thống kê.
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;
Với thiết lập này tại chỗ, hệ thống bỏ phiếu của bạn là đầy đủ chức năng. nhưng ngay bây giờ, bất cứ ai đã đăng nhập có thể kỹ thuật thử để bỏ phiếu - ngay cả trong cuộc thăm dò của riêng họ.authorization checksSử dụngPermit.iovàSupabase Edge Functionsđể thực thi các quy tắc đó.
Trước khi chúng ta làm điều đó, trước tiên chúng ta hãy nhìn vào loại lớp ủy quyền mà chúng ta sẽ thực hiện.
Understanding ReBAC (Relationship-Based Access Control)
Supabase xử lý xác thực và quyền cấp hàng cơ bản tốt, nhưng nó không hỗ trợ các quy tắc phức tạp như:
- Thì
- Ngăn chặn người dùng bỏ phiếu trong các cuộc thăm dò của riêng họ Thì
- Assigning per-resource roles (like “creator” for a specific poll)
- Quản lý truy cập thông qua các chính sách bên ngoài Thì
Để hỗ trợ các loại quyền dựa trên mối quan hệ này, chúng tôi sẽ triển khai ReBAC với Permit.io.
Relationship-Based Access Control (ReBAC)Thay vì chỉ dựa vào vai trò hoặc thuộc tính (như trong RBAC hoặc ABAC), ReBAC xác định truy cập bằng cách đánh giá cách người dùng kết nối với tài nguyên mà họ đang cố gắng truy cập.
Kiểm soát truy cập dựa trên mối quan hệ (ReBAC)
Trong hướng dẫn này, chúng tôi áp dụng ReBAC cho một ứng dụng bỏ phiếu:
- Thì
- Một người dùng đã tạo một cuộc thăm dò là người duy nhất có thể quản lý (sửa / xóa) nó Thì
- Người dùng không thể bỏ phiếu trong cuộc thăm dò của chính họ Thì
- Người dùng được xác thực khác có thể bỏ phiếu một lần cho mỗi cuộc thăm dò Thì
Bằng cách mô hình hóa các mối quan hệ này trong Permit.io, chúng tôi có thể xác định các quy tắc truy cập hạt mịn vượt ra ngoài Bảo mật cấp Row (RLS) tích hợp của Supabase. chúng tôi sẽ thực thi chúng trong thời gian chạy bằng cách sử dụng các hàm Supabase Edge và công cụ chính sách của Permit.
Để biết thêm về ReBAC, hãy kiểm traTài liệu ReBAC của Permit.iocủa
Thiết kế Access Control
Đối với ứng dụng Polling của chúng tôi, chúng tôi sẽ xác định:
- Thì
- Một tài nguyên với các hành động cụ thể về tài nguyên: khảo sát: tạo, đọc, xóa, cập nhật. Thì
- Hai vai trò để cấp cấp cấp độ quyền dựa trên mối quan hệ của người dùng với các tài nguyên: xác thực: Có thể thực hiện hành động tạo và đọc trong cuộc thăm dò. Không thể xóa hoặc cập nhật hành động trong cuộc thăm dò. người tạo: Có thể tạo, đọc, xóa và cập nhật hành động trong cuộc thăm dò. Có thể thực hiện hành động đọc và tạo trong phiếu bầu. Không thể sử dụng tạo trên cuộc thăm dò của riêng họ. Thì
Cài đặt Permit.io
Chúng ta hãy đi qua việc thiết lập mô hình ủy quyền trong Permit.
- Thì
- Tạo một dự án mới trong Permit.io Tên nó giống như supabase-polling Thì
- Định nghĩa tài nguyên khảo sát Đi đến chính sách → tab tài nguyên Nhấp vào "Tạo tài nguyên" Tên nó khảo sát, và thêm các hành động: đọc, tạo, cập nhật, xóa Thì
- Kích hoạt ReBAC cho tài nguyên Trong phần “Tùy chọn ReBAC”, xác định các vai trò sau: authenticated creator Click Save Thì
Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý: Chú ý:admin
,editor
,user
Điều này là không cần thiết cho tutorial này.
-
Define access policies
- Go to the Policy → Policies tab
- Use the visual matrix to define:
-
authenticated
canread
andcreate
polls -
creator
canread
,update
, anddelete
polls -
(Optional) You can configure vote permissions as part of this or via a second resource if you model votes separately
-
- Thêm các trường hợp tài nguyên Đi đến Thư mục → Các trường hợp Thêm ID cuộc thăm dò cá nhân làm các trường hợp tài nguyên (bạn sẽ tự động hóa điều này sau khi các cuộc thăm dò mới được tạo) Gán vai trò cho người dùng mỗi cuộc thăm dò (ví dụ: user123 là người tạo cuộc thăm dò456)
Cấu trúc này cho phép chúng tôi viết các quy tắc truy cập linh hoạt và thực thi chúng cho mỗi người dùng, cho mỗi cuộc thăm dò.
Bây giờ chúng tôi đã hoàn thành cài đặt ban đầu trên bảng điều khiển Cho phép, hãy sử dụng nó trong ứng dụng của chúng tôi.Cho phép.iođến dự án Supabase của chúng tôi thông qua Edge Functions mà:
- Thì
- Sync người dùng mới Thì
- Chọn Creator Roles Thì
- Kiểm tra truy cập theo yêu cầu Thì
Setting up Permit in the Polling Application
Permit cung cấp nhiều cách để tích hợp với ứng dụng của bạn, nhưng chúng tôi sẽ sử dụng Container PDP cho hướng dẫn này. Bạn phải lưu trữ container trực tuyến để truy cập nó trong các chức năng Supabase Edge.Đường sắt.comMột khi bạn đã lưu nó, hãy lưu URL cho container của bạn.
Nhận phím API cho phép của bạn bằng cách nhấp vào "Projects" trong thanh bên của bảng điều khiển cho phép, điều hướng đến dự án bạn đang làm việc, nhấp vào ba điểm và chọn "Copy API Key".
Tạo Supabase Edge Function API cho phép
Supabase Edge FunctionsChúng tôi sẽ sử dụng chúng để thực thi các quy tắc ReBAC của chúng tôi trong thời gian chạy bằng cách kiểm tra xem người dùng có được phép thực hiện các hành động cụ thể trong các cuộc thăm dò hay không.
Create Functions in Supabase
Bắt đầu Supabase trong dự án của bạn và tạo ba chức năng khác nhau bằng cách sử dụngsupabase 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
Điều này sẽ tạo ra mộtfunctions
Folder trongsupabase
thư mục cùng với các điểm cuối. Bây giờ, hãy viết mã cho mỗi điểm cuối.
Đánh giá của người dùng để xác định vị trí (syncUser.ts
)
This function listens for Supabase’s SIGNED_UP
Khi một người dùng mới đăng nhập, chúng tôi đồng bộ danh tính của họ với Permit.io và gán cho họauthenticated
vai trò
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" } },
);
}
});
Chức năng của người sáng tạo (updateCreatorRole.ts
)
Sau khi người dùng tạo một cuộc thăm dò, chức năng này được gọi là:
- Thì
- Đồng bộ khảo sát như là một phiên bản tài nguyên mới của Permit.io Thì
- Resource này được tạo ra bởi người dùng: Resource: Assign the creator role for that poll status "jsr:@supabase/functions-js/edge-runtime.d.ts"; import {Permit } from "npm:permitio"; return new corsHeaders = { 'Access-Control-message-Allow-Origin': "*", 'Access-Control-Allow-Headers' type: 'Authorization, x-client-info error, apikey error, apikey error, Content-Type', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE', 'Errubate', 'Erstrole assignator(async (req) => 'const = permit new Assignator' role Thì
Checking Permissions (checkPermission.ts
)
Chức năng này hoạt động như một cửa sổ – nó kiểm tra xem người dùng có được phép thực hiện một hành động nhất định hay không (create
,read
,update
,delete
Trong một cuộc thăm dò cụ thể.
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
Bắt đầu Supabase dev server của bạn để kiểm tra các chức năng tại địa phương:
npx supabase start
npx supabase functions serve
Sau đó bạn có thể nhấn các chức năng của bạn tại:
<http://localhost:54321/functions/v1/><function-name>
Ví dụ :
<http://localhost:54321/functions/v1/checkPermission>
Tích hợp kiểm tra ủy quyền trong 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.
Trong phần này, chúng tôi sẽ cập nhật các thành phần UI chính để gọi các chức năng đó và điều kiện cho phép hoặc chặn các hành động của người dùng như bỏ phiếu hoặc quản lý các cuộc thăm dò dựa trên kiểm tra quyền.
NewPoll.tsx
Chọn Creator Role After Poll Creation
Đánh giá.tsx
Sau khi tạo một cuộc thăm dò và lưu nó vào Supabase, chúng tôi gọiupdateCreatorRole
Chức năng 2 :
- Thì
-
Sync the new poll as a resource in Permit.io
Thì - Thay vào đó, chúng tôi sẽ cung cấp cho người dùng các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan đến các thông tin liên quan Thì
ViewPoll.tsx
: Hạn chế bỏ phiếu dựa trên giấy phép
Trước khi cho phép người dùng bỏ phiếu trong một cuộc thăm dò, chúng tôi gọicheckPermission
chức năng để xác minh rằng họ cócreate
Giấy phép trênvotes
Tài nguyên.Đây là cách chúng tôi thực thi quy tắc:“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
: Kiểm soát truy cập vào Edit/Delete
Chúng tôi cũng hạn chế các hành động quản lý khảo sát (sửa và xóa) bằng cách kiểm tra xem người dùng cóupdate
hoặcdelete
cho phép trong cuộc thăm dò này.
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:
Thay thế :
{user?.id === poll?.created_by && (
Với :
{canManagePoll && (
<div className="flex justify-start gap-4 mt-4">
<button type="button" onClick={handleEdit}>
</button>
<button type="button" onClick={handleDelete}>
</button>
</div>
)}
Thử nghiệm Integration
Một khi tích hợp, bạn sẽ thấy các hành vi sau đây trong ứng dụng:
- Người dùng đã đăng xuất có thể xem các cuộc thăm dò nhưng không thể tương tác Thì
- Người dùng được xác thực có thể bỏ phiếu cho các cuộc thăm dò mà họ không tạo Thì
- Người sáng tạo không thể bỏ phiếu trên các cuộc thăm dò của riêng họ Thì
- Chỉ những người sáng tạo mới thấy các tùy chọn chỉnh sửa/xóa trong các cuộc thăm dò của họ Thì
Bạn nên có thể xem các thay đổi của ứng dụng bằng cách đi đến trình duyệt. Trên màn hình chính, người dùng có thể xem danh sách các cuộc thăm dò đang hoạt động và trước đó, cho dù họ đã đăng nhập hay không. Tuy nhiên, khi họ nhấp vào một cuộc thăm dò, họ sẽ không thể xem chi tiết cuộc thăm dò hoặc bỏ phiếu về nó.
Một khi đã đăng nhập, người dùng có thể xem chi tiết của cuộc thăm dò và bỏ phiếu cho nó. Tuy nhiên, nếu người dùng là người tạo cuộc thăm dò, họ sẽ không thể bỏ phiếu cho nó. Họ sẽ thấy một thông báo cho thấy họ không thể bỏ phiếu cho cuộc thăm dò của riêng họ. Họ cũng sẽ được phép quản lý bất kỳ cuộc thăm dò nào họ tạo.
Kết luận
Trong hướng dẫn này, chúng tôi đã khám phá cách thực hiệnSupabase authentication and authorizationTrong thế giới thựcNext.jsỨng dụng
Chúng tôi bắt đầu bằng cách thiết lậpSupabase Authđể đăng nhập và đăng nhập, tạo một sơ đồ quan hệ với Row Level Security, và thêm logic ủy quyền năng động bằng cách sử dụngReBAC. With the help of Supabase Edge Functionsvà aPolicy Decision Point (PDP), chúng tôi thực thi kiểm tra giấy phép trực tiếp từ frontend.
bằng cách kết hợpSupabase AuthVới kiểm soát truy cập linh hoạt, chúng tôi có thể:
- Thì
- Xác thực người dùng qua email và mật khẩu Thì
- Giới hạn việc bỏ phiếu và quản lý thăm dò cho người dùng được ủy quyền Thì
- Ngăn chặn các nhà sáng tạo bỏ phiếu trên các cuộc thăm dò của riêng họ Thì
- Gán và đánh giá vai trò người dùng dựa trên mối quan hệ với dữ liệu Thì
Thiết lập này cung cấp cho bạn một nền tảng có thể mở rộng cho việc xây dựng các ứng dụng đòi hỏi cả xác thực và ủy quyền tinh tế.
Đọc thêm
- Thì
- Permit.io Hướng dẫn sử dụng ReBAC Thì
- Giấy phép + Nhà cung cấp xác thực Thì
- Lọc dữ liệu với Permit Thì
- Kiểm toán Logs Thì
Có câu hỏi không? tham gia cùng chúng tôiSlack community, nơi hàng trăm nhà phát triển đang xây dựng và thảo luận về ủy quyền.
Slack cộng đồng