Authorization, RLS, and Admin
Day 5 - ชั่วโมงที่ 3: กำหนดสิทธิ์ USER และ ADMIN
Hour 2 ทำให้ระบบรู้ว่าใคร Login อยู่ ชั่วโมงนี้เราจะกำหนดว่าผู้ใช้แต่ละ Role อ่านหรือแก้ข้อมูลใดได้บ้าง
Supabase SQL Editor สร้าง profiles, created_by และ RLS Policies
lib/auth.ts เพิ่มการอ่าน Role และ requireAdmin()
lib/issues.ts บันทึกเจ้าของ Issue
app/actions.ts ตรวจ User และ ADMIN
app/issues/page.tsx ป้องกันหน้ารายการ
app/issues/[id]/page.tsx ป้องกันหน้ารายละเอียด
components/IssueList.tsx ซ่อนเครื่องมือจัดการในหน้า USER
app/admin/issues/page.tsx สร้างใหม่
components/AppNav.tsx เพิ่มลิงก์ AdminSlide 1: จาก Authentication สู่ Authorization
Hour 2 ตอบคำถามแรกได้แล้ว:
Authentication → ผู้ใช้คนนี้คือใคร?แต่การ Login สำเร็จไม่ได้หมายความว่าทุกคนควรทำงานได้เหมือนกัน เราจึงต้องตอบคำถามต่อไป:
Authorization → ผู้ใช้คนนี้ทำอะไรกับข้อมูลใดได้บ้าง?Slide 2: กำหนดสิทธิ์ก่อนเขียน Code
| Role | ดูข้อมูล | สร้าง Issue | เปลี่ยน Status |
|---|---|---|---|
| USER | เฉพาะ Issue ของตัวเอง | ได้ | ไม่ได้ |
| ADMIN | Issue ทั้งหมด | ได้ | ได้ |
ระบบจะตรวจสิทธิ์สามชั้น:
UI → แสดงเฉพาะคำสั่งที่เหมาะกับผู้ใช้
Server Action → ตรวจ Role ก่อนทำงานสำคัญ
RLS → Database ตรวจทุก Row ก่อนอ่านหรือเขียนSlide 3: เพิ่มเจ้าของให้แต่ละ Issue
ตอนนี้ Table issues ยังไม่รู้ว่าแต่ละ Row เป็นของใคร จึงยังสร้างกฎ “USER เห็นเฉพาะของตัวเอง” ไม่ได้
เมื่อ Login แล้ว auth.uid() จะคืน ID ของผู้ใช้ปัจจุบัน เราจะเก็บ ID เดียวกันไว้ใน issues.created_by
auth.users.id ──ผู้สร้าง──> issues.created_byalter table public.issues
add column if not exists created_by uuid
references auth.users(id) on delete set null;
create index if not exists issues_created_by_idx
on public.issues (created_by);references auth.users(id)บังคับให้ใช้ ID ของบัญชีที่มีอยู่จริงon delete set nullเก็บ Issue ไว้แม้บัญชีผู้สร้างถูกลบcreate index ... (created_by)สร้างตัวช่วยค้นหาที่ Column เจ้าของ เพราะ RLS ต้องเปรียบเทียบcreated_byทุกครั้งที่อ่าน Issue
เมื่อข้อมูลมีจำนวนมาก Index ช่วยให้ Database ไปยัง Row ของผู้ใช้ได้เร็วขึ้น แทนการไล่ตรวจทุก Row ใน Table
Issue เก่าจาก Day 4 จะมี created_by = null ซึ่งเป็นผลลัพธ์ที่ถูกต้อง เพราะข้อมูลเหล่านั้นถูกสร้างก่อนมีระบบผู้ใช้
Slide 4: แยกข้อมูล Login ออกจาก Role
Supabase สร้าง auth.users ให้อัตโนมัติเมื่อเราเพิ่มบัญชีผ่าน Authentication → Users Table นี้ดูแล Email, Password และตัวตนของผู้ใช้
Role เป็นข้อมูลเฉพาะของ Issue Tracker เราจึงสร้าง profiles แยกออกมาและใช้ ID เดียวกันเชื่อมสอง Table
auth.users → ตัวตนและการ Login
profiles → Role ของ Issue Trackercreate table if not exists public.profiles (
id uuid primary key references auth.users(id) on delete cascade,
email text,
role text not null default 'USER'
check (role in ('USER', 'ADMIN')),
created_at timestamptz not null default now()
);idเป็นทั้ง Primary Key และ Foreign Key จึงมีได้หนึ่ง Profile ต่อหนึ่ง Useron delete cascadeลบ Profile ตามอัตโนมัติเมื่อลบบัญชีที่เชื่อมอยู่ในauth.usersจึงไม่เหลือ Profile ที่ไม่มีเจ้าของ- Role เริ่มต้นเป็น
USERซึ่งมีสิทธิ์น้อยกว่า checkป้องกันค่าอื่นหรือการสะกด Role ผิด
Slide 5: สร้าง Profile และตั้ง ADMIN
บัญชีจาก Hour 2 อยู่ใน auth.users แล้ว แต่ profiles ที่เพิ่งสร้างยังไม่มีข้อมูล ขั้นตอนนี้จะคัดลอก ID และ Email มาสร้าง Profile
1. สร้าง Profile ให้บัญชีที่มีอยู่
insert into public.profiles (id, email, role)
select id, email, 'USER'
from auth.users
on conflict (id) do update
set email = excluded.email;select ... from auth.users อ่านบัญชีที่สร้างไว้ ส่วน on conflict ทำให้รันซ้ำได้โดยไม่สร้าง Profile ซ้ำและไม่เปลี่ยน ADMIN กลับเป็น USER
2. ตั้งบัญชีผู้ดูแล
update public.profiles
set role = 'ADMIN'
where email = 'admin@example.com';3. ตรวจผลลัพธ์
select id, email, role
from public.profiles
order by email;ควรเห็น user@example.com เป็น USER และ admin@example.com เป็น ADMIN
Slide 6: ให้ผู้ใช้เห็นเฉพาะ Profile ตัวเอง
แอปต้องอ่าน Role ของผู้ใช้ แต่ไม่ควรเปิดให้ผู้ใช้เห็น Profile ของคนอื่นหรือแก้ Role ของตัวเอง
alter table public.profiles enable row level security;
revoke all on public.profiles from anon, authenticated;
grant select on public.profiles to authenticated;
drop policy if exists "users_can_view_own_profile"
on public.profiles;
create policy "users_can_view_own_profile"
on public.profiles
for select
to authenticated
using (id = (select auth.uid()));อ่านคำสั่งตามลำดับ
| คำสั่ง | หน้าที่ |
|---|---|
enable row level security | เปิดให้ Table ตรวจ RLS Policy ทุกครั้งที่เข้าถึงข้อมูล |
revoke all ... from anon, authenticated | ถอนสิทธิ์เดิมของทั้งผู้ที่ยังไม่ Login และผู้ที่ Login แล้ว เพื่อเริ่มกำหนดใหม่ให้ชัดเจน |
grant select ... to authenticated | ให้เฉพาะผู้ที่ Login แล้วมีสิทธิ์ส่งคำสั่งอ่าน Table |
drop policy if exists | ลบ Policy ชื่อเดิมก่อน ทำให้รัน SQL ชุดนี้ซ้ำได้โดยไม่ชนชื่อ |
for select to authenticated | ใช้ Policy นี้เฉพาะการอ่านข้อมูลของผู้ที่ Login แล้ว |
using (id = (select auth.uid())) | อนุญาตเฉพาะ Row ที่ Profile ID ตรงกับ ID ใน Session |
ยังไม่ Login → ไม่มีสิทธิ์อ่าน profiles
Login แล้ว → อ่านได้เฉพาะ Row ที่ id ตรงกับ auth.uid()Policy users_can_view_own_profile เป็น Policy ใหม่ที่เราสร้างใน Slide นี้ และเพราะไม่ได้ให้สิทธิ์ update ผู้ใช้จึงเปลี่ยนตัวเองเป็น ADMIN ผ่านแอปไม่ได้
Slide 7: สร้างกฎ SELECT สำหรับ Issue
กฎแรกควบคุม .select() โดย USER เห็น Row ที่ตัวเองสร้าง ส่วน ADMIN เห็นทุก Row
drop policy if exists "users_view_own_admins_view_all"
on public.issues;
create policy "users_view_own_admins_view_all"
on public.issues
for select
to authenticated
using (
created_by = (select auth.uid())
or exists (
select 1
from public.profiles
where id = (select auth.uid())
and role = 'ADMIN'
)
);อ่านเงื่อนไขเป็นภาษาง่าย ๆ:
เป็นเจ้าของ Issue หรือเป็น ADMIN → อ่าน Row นี้ได้
ไม่ตรงทั้งสองเงื่อนไข → Database ไม่ส่ง Row นี้กลับมาexists (...) ตรวจ Profile ของผู้ใช้ปัจจุบันว่ามี Role เป็น ADMIN หรือไม่ โดยผู้ใช้มีสิทธิ์อ่านเฉพาะ Profile ของตัวเองจาก Slide 6
ตอนนี้ Demo Policy เดิมยังเปิดอยู่ Policy ใหม่นี้จึงยังไม่จำกัดข้อมูลจริง เราจะปิด Demo Access หลังเตรียม Code และ Policy ทุกส่วนเสร็จใน Slide 18
Slide 8: สร้างกฎ INSERT สำหรับเจ้าของ Issue
ตอนสร้าง Issue แอปต้องส่ง created_by ที่ตรงกับผู้ใช้ใน Session Database จึงควรตรวจค่านี้ก่อนบันทึก
drop policy if exists "users_insert_own_issues"
on public.issues;
create policy "users_insert_own_issues"
on public.issues
for insert
to authenticated
with check (created_by = (select auth.uid()));with check ตรวจ Row ใหม่ก่อนบันทึก ถ้า created_by ไม่ตรงกับผู้ใช้ที่ Login Database จะปฏิเสธ
created_by ตรงกับ auth.uid() → บันทึกได้
created_by เป็น ID อื่นหรือเป็น null → ปฏิเสธPolicy นี้ช่วยป้องกันทั้งกรณีที่แอปลืมส่งเจ้าของ และกรณีที่ผู้ใช้พยายามสร้าง Issue ในนามของคนอื่น
Slide 9: สร้างกฎ UPDATE สำหรับ ADMIN
USER มีหน้าที่แจ้งและติดตาม Issue ส่วนการเปลี่ยน Status เป็นหน้าที่ของ ADMIN
drop policy if exists "admins_update_issues"
on public.issues;
create policy "admins_update_issues"
on public.issues
for update
to authenticated
using (
exists (
select 1 from public.profiles
where id = (select auth.uid()) and role = 'ADMIN'
)
)
with check (
exists (
select 1 from public.profiles
where id = (select auth.uid()) and role = 'ADMIN'
)
);usingตรวจว่า ADMIN เลือก Row เดิมมาแก้ได้หรือไม่with checkตรวจอีกครั้งก่อนบันทึก Row หลังแก้- ไม่มี DELETE Policy เพราะระบบใช้ Status
DONEแทนการลบ Issue
Slide 10: บันทึกเจ้าของตอนสร้าง Issue
Policy INSERT ต้องได้รับ created_by ที่ตรงกับ Session เราจึงนำ user.id จาก requireUser() ส่งต่อไปยัง createIssue()
Session → requireUser() → user.id → createIssue() → created_byจุดที่ 1: แก้ createIssueAction() ใน app/actions.ts
export async function createIssueAction(formData: FormData) { await requireUser(); const user = await requireUser(); const input = parseIssueInput(formData); // เก็บ Validation เดิมไว้ await createIssue(input); await createIssue(input, user.id);}
จุดที่ 2: เพิ่มสองบรรทัดใน createIssue() ที่ lib/issues.ts
เก็บ Promise<void>, Fields และ Error Handling เดิมไว้ แล้วเพิ่มเฉพาะบรรทัดที่ Highlight:
export async function createIssue( input: NewIssueInput, userId: string,): Promise<void> { const supabase = await createClient(); const { error } = await supabase.from("issues").insert({ reporter_name: input.reporterName, reporter_email: input.reporterEmail, title: input.title, description: input.description, status: "OPEN", created_by: userId, }); if (error) { throw new Error(`Failed to create issue: ${error.message}`); }}
สิ่งที่เพิ่มมีเพียง:
userId: stringทำให้ Function รับ ID ของผู้ใช้จาก Server Actioncreated_by: userIdบันทึก ID นั้นเป็นเจ้าของ Row ใหม่
อย่ารับ created_by จาก Hidden Input เพราะผู้ใช้แก้ค่าใน Browser ได้ ID ต้องมาจาก Session และ RLS จะตรวจซ้ำก่อนบันทึก
เราไม่เพิ่ม createdBy ใน Type Issue เพราะ UI ยังไม่ได้ใช้ Field นี้ Database ใช้ created_by สำหรับ RLS และการบันทึกเจ้าของโดยตรง
Slide 11: เพิ่ม Helper ตรวจสิทธิ์ ADMIN
จากโค้ดเดิม ไม่ต้องลบหรือแก้ getCurrentUser() และ requireUser() เราจะเพิ่มสามส่วน:
UserRoleกำหนดค่า Role ที่แอปรองรับgetCurrentUserRole()อ่าน Role จากprofilesrequireAdmin()บังคับว่าต้อง Login และมี Role เป็น ADMIN
จุดที่ 1: เพิ่ม Type ใต้ Import เดิม
import { redirect } from "next/navigation";import { createClient } from "@/lib/supabase/server"; type UserRole = "USER" | "ADMIN";
จุดที่ 2: เพิ่มสอง Functions ต่อจาก requireUser()
export async function getCurrentUserRole(
userId: string,
): Promise<UserRole | null> {
const supabase = await createClient();
const { data, error } = await supabase
.from("profiles")
.select("role")
.eq("id", userId)
.maybeSingle();
if (error) {
console.error("Failed to read user role:", error.message);
return null;
}
if (data?.role === "USER" || data?.role === "ADMIN") {
return data.role;
}
return null;
}
export async function requireAdmin() {
const user = await requireUser();
const role = await getCurrentUserRole(user.id);
if (role !== "ADMIN") {
redirect("/issues");
}
return user;
}ไม่มี Session → requireUser() ส่งไป /login
มี Role USER → requireAdmin() ส่งไป /issues
มี Role ADMIN → ทำงานต่อRole ถูกอ่านจาก Database ฝั่ง Server ไม่ได้เชื่อค่าจาก URL, Email หรือ Browser
Slide 12: ป้องกันหน้าที่อ่าน Issue
Hour 2 ป้องกัน /issues/new แล้ว แต่หน้ารายการและรายละเอียดก็ต้อง Login ก่อน Query เพราะ Slide 18 จะถอนสิทธิ์ของ anon
แก้ app/issues/page.tsx
import { IssueList } from "@/components/IssueList";import { getIssues } from "@/lib/issues";import { requireUser } from "@/lib/auth"; export default async function IssuesPage() { await requireUser(); const issues = await getIssues(); // เก็บ JSX เดิมไว้}
แก้ app/issues/[id]/page.tsx
import { notFound } from "next/navigation";import { getIssueById } from "@/lib/issues";import { requireUser } from "@/lib/auth"; export default async function IssueDetailPage({ params,}: IssueDetailPageProps) { await requireUser(); const { id } = await params; const issue = await getIssueById(id); // เก็บ notFound() และ JSX เดิมไว้}
Slide 13: ให้เฉพาะ ADMIN เปลี่ยน Status ได้
updateIssueStatusAction() เดิมตรวจว่า id และ status ถูกต้อง แต่ยังไม่ได้ตรวจว่า ใครเป็นคนเรียก Action ถ้า USER ส่ง Request เข้ามาเอง Code เดิมจะพยายาม Update Database
แก้สามจุดใน app/actions.ts
| จุดที่แก้ | เหตุผล |
|---|---|
Import requireAdmin | เพื่อเรียก Helper ที่สร้างใน Slide 11 |
await requireAdmin() | ปฏิเสธ USER ก่อนอ่าน formData และ Update |
revalidatePath("/admin/issues") | ให้หน้า Admin แสดง Status ล่าสุด |
import { requireUser } from "@/lib/auth";import { requireAdmin, requireUser } from "@/lib/auth"; export async function updateIssueStatusAction( formData: FormData,): Promise<void> { await requireAdmin(); const id = String(formData.get("id") ?? "").trim(); // เก็บ Validation และ Update เดิมไว้ revalidatePath("/issues"); revalidatePath("/admin/issues");}
เก็บ requireUser ไว้เพราะ createIssueAction() ยังใช้งานอยู่ ส่วน RLS จะตรวจ Role ซ้ำอีกชั้นก่อน Database แก้ Row
Slide 14: ให้ IssueList เลือกแสดงปุ่มจัดการ
IssueList จาก Day 4 แสดงปุ่มเปลี่ยน Status อยู่เสมอ แต่หลังแบ่งสิทธิ์แล้วสองหน้าต้องแสดงผลต่างกัน:
หน้า /issues → USER ดูข้อมูลอย่างเดียว
หน้า /admin/issues → ADMIN เห็นปุ่มเปลี่ยน Statusเราจึงเพิ่ม Prop canManage เพื่อให้ Parent บอกว่า Context นี้แสดงเครื่องมือจัดการได้หรือไม่
type IssueListProps = { issues: Issue[]; canManage?: boolean;}; export function IssueList({ issues }: IssueListProps) {export function IssueList({ issues, canManage = false,}: IssueListProps) {
canManage?เป็น Optional Prop จึงไม่บังคับให้ทุกหน้าต้องส่งค่าcanManage = falseทำให้หน้าที่ไม่ส่ง Prop แสดงรายการแบบอ่านอย่างเดียว- หน้า Admin จะส่ง
<IssueList issues={issues} canManage />เพื่อเปลี่ยนค่าเป็นtrue
Slide นี้เตรียมค่า canManage ก่อน ส่วน Slide 15 จะนำค่านี้ไปควบคุมหัว Column และปุ่มในแต่ละ Row
Slide 15: ซ่อน Column จัดการด้วย canManage
Slide ก่อนหน้าเป็นเพียงการเตรียม Prop ตอนนี้ให้นำ canManage มาใช้ใน JSX เพื่อเลือกว่าจะแสดง Column จัดการหรือไม่
canManageเป็นtrue→ แสดงหัว Column และปุ่มเปลี่ยน StatuscanManageเป็นfalse→ ไม่สร้าง Element เหล่านี้บนหน้าเว็บ
จุดที่ 1: แทน <th> เดิมใน <thead>
<th className="px-3 py-3 font-semibold">จัดการ</th>{canManage && ( <th className="px-3 py-3 font-semibold">จัดการ</th>)}
จุดที่ 2: ครอบ <td> เดิมภายในแต่ละ Row
<td className="px-3 py-4"> {/* Forms และปุ่มเปลี่ยน Status เดิม */}</td>{canManage && ( <td className="px-3 py-4"> {/* เก็บ Forms และปุ่มเปลี่ยน Status เดิมทั้งหมดไว้ */} </td>)}
ให้นำ <th> และ <td> เดิมมาครอบด้วยเงื่อนไข ไม่ต้องสร้าง Column ชุดที่สอง และต้องใช้เงื่อนไขเดียวกันทั้งสองจุดเพื่อให้หัวตารางตรงกับข้อมูล
การซ่อนปุ่มช่วยให้หน้า USER อ่านง่ายขึ้น แต่ Security จริงยังต้องตรวจซ้ำใน Server Action และ RLS
Slide 16: สร้างหน้า /admin/issues
import { IssueList } from "@/components/IssueList";
import { requireAdmin } from "@/lib/auth";
import { getIssues } from "@/lib/issues";
export default async function AdminIssuesPage() {
await requireAdmin();
const issues = await getIssues();
return (
<main className="mx-auto max-w-5xl px-6 py-8">
<h1 className="text-2xl font-bold">Admin Issues</h1>
<p className="mt-2 text-sm text-slate-600">
ดู Issue ทั้งหมดและอัปเดตสถานะ
</p>
<div className="mt-6">
<IssueList issues={issues} canManage />
</div>
</main>
);
}requireAdmin() → getIssues() → RLS ให้ ADMIN เห็นทุก Row → canManage แสดงปุ่มหาก USER พิมพ์ URL นี้โดยตรง requireAdmin() จะส่งกลับ /issues
Slide 17: แสดงลิงก์ Admin ตาม Role
เพิ่ม Role เข้าไปใน AppNav() เพื่อให้เฉพาะ ADMIN เห็นทางเข้าหน้าจัดการ
1. แก้ Import
import { getCurrentUser } from "@/lib/auth";import { getCurrentUser, getCurrentUserRole } from "@/lib/auth";
2. อ่าน Role ภายใน AppNav()
export async function AppNav() { const user = await getCurrentUser(); const role = user ? await getCurrentUserRole(user.id) : null; return (
3. แทรกลิงก์ Admin ก่อนลิงก์ New Issue
ภายในเงื่อนไข {user ? (...)} ให้แทรกโค้ดใหม่หลัง <> และก่อน <Link href="/issues/new">
{user ? ( <> {role === "ADMIN" && ( <Link href="/admin/issues" className="font-semibold"> Admin </Link> )} <Link href="/issues/new" className="font-semibold"> New Issue </Link> {/* เก็บ Email และปุ่ม Logout เดิมไว้ */} </>) : ( // เก็บลิงก์ Login เดิมไว้)}
ลิงก์ New Issue, Email และปุ่ม Logout เดิมไม่ต้องลบ เพียงเพิ่มเงื่อนไขของ Admin ไว้ข้างหน้าลิงก์ New Issue
การซ่อนลิงก์ช่วยไม่ให้ USER สับสน แต่ requireAdmin() ในหน้า Admin ยังเป็นผู้ตรวจสิทธิ์จริง
Slide 18: สลับจาก Demo Policy เป็นสิทธิ์จริง
ตอนนี้ Auth, Role และ Policies ใหม่พร้อมแล้ว ขั้นสุดท้ายคือปิดสิทธิ์ชั่วคราวจาก Day 4
Policy ของคำสั่งเดียวกันทำงานแบบ OR หาก Demo Policy ที่ใช้ true ยังอยู่ ทุก Row จะยังผ่านได้ แม้เราสร้าง Policy ใหม่แล้ว
Demo Policy (true) OR Policy ใหม่ = ผ่าน1. ลบ Demo Policies เดิม
drop policy if exists "demo_select_issues" on public.issues;
drop policy if exists "demo_insert_issues" on public.issues;
drop policy if exists "demo_update_issues" on public.issues;if exists ช่วยให้รันคำสั่งซ้ำได้โดยไม่ Error หาก Policy นั้นถูกลบไปแล้ว
2. อนุญาตเฉพาะ Request ที่ Login แล้ว
แม้หน้า Policies จะเหลือเฉพาะกฎของ authenticated แล้ว ก็ยังต้องรันขั้นตอนนี้ เพราะหน้า Policies แสดงกฎ RLS แต่ไม่ได้ยืนยันว่า anon ถูกถอนสิทธิ์ระดับ Table แล้ว
revoke select, insert, update on public.issues from anon;
grant select, insert, update on public.issues to authenticated;revoke ... from anonถอนสิทธิ์ชั่วคราวที่เราเคยให้ไว้ใน Day 4grant ... to authenticatedยืนยันว่าผู้ที่ Login แล้วส่งคำสั่งมาที่ Table ได้ คำสั่งนี้รันซ้ำได้- RLS จะตรวจต่อว่าผู้ใช้คนนั้นทำงานกับ Row ใดได้บ้าง
หลัง Slide นี้ระบบจะบังคับสิทธิ์จริง หากหน้าเว็บมี Error ให้ตรวจ Policies และ Code ตาม Slide ก่อนหน้าแทนการเปิด Demo Policy กลับมา
Slide 19: ทดสอบ USER และ ADMIN
Logout ก่อนสลับบัญชี เพื่อไม่ให้ Session เดิมค้างอยู่
ทดสอบ user@example.com
- Login แล้วสร้าง Issue ใหม่
- หน้า
/issuesต้องเห็นเฉพาะ Issue ที่บัญชีนี้สร้าง - ต้องไม่เห็น Column จัดการหรือลิงก์ Admin
- เปิด
/admin/issuesโดยตรง → ต้องกลับ/issues
ทดสอบ admin@example.com
- ต้องเห็นลิงก์ Admin
- หน้า
/admin/issuesต้องเห็น Issue ทั้งหมด รวมข้อมูลเก่าที่created_byเป็นnull - เปลี่ยน Status แล้วข้อมูลต้องอัปเดต
ทดสอบหลัง Logout
- เปิด
/issues,/issues/[id]หรือ/issues/new→ ต้องไป/login
ถ้าผลลัพธ์ไม่ตรง
| อาการ | จุดที่ควรตรวจ |
|---|---|
permission denied | GRANT และ Role ของ Request |
new row violates row-level security | user.id, created_by และ INSERT Policy |
| สร้างแล้วรายการว่าง | created_by ของ Row ล่าสุดและ SELECT Policy |
| อ่าน Role ไม่ได้ | ข้อมูลและ Policy ของ profiles |
| ADMIN เข้าไม่ได้ | Role ของ admin@example.com และ requireAdmin() |
Slide 20: Authorization Flow พร้อมใช้งาน
Login → Session → Profile Role → Server Check → RLS → ข้อมูลที่ได้รับอนุญาตHour 4 จะทบทวน Security ทั้งระบบ ตรวจค่าที่เปิดเผยได้ และทดสอบ Flow บน Production