Files
OG/db/auth-hook.sql
T
Weei HanandClaude Sonnet 5 f539d43136 M0: DB schema, RLS policies, JWT claims hook, seed, Supabase clients
Adds db/schema.sql (12 tables), db/policies.sql (RLS on all 12,
audit_log append-only), db/auth-hook.sql (role/org_id into JWT per
AD-2), db/seed.sql (org + 3 departments, part 2 deferred to M1 auth).
Wires lib/supabase/{client,server,service}.ts per AD-3 and adds
/db-check page confirming DB connectivity and RLS deny-by-default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FcktbLXSSXzx23GCue813e
2026-07-30 19:00:24 +08:00

80 lines
2.5 KiB
PL/PgSQL

-- Custom Access Token Hook
-- Implements AD-2 from docs/03-architecture.md:
-- "A Postgres function copies role/org_id into JWT claims (custom
-- access token hook) so RLS can check them cheaply."
--
-- Run this AFTER schema.sql (needs the profiles table to exist).
-- After running, enable the hook in the Supabase dashboard (see bottom
-- of this file for the exact steps) — running the SQL alone does not
-- activate it.
create or replace function public.custom_access_token_hook(event jsonb)
returns jsonb
language plpgsql
stable
as $$
declare
claims jsonb;
user_role public.user_role;
user_org_id uuid;
begin
-- Look up this user's role and org_id from profiles.
select role, org_id
into user_role, user_org_id
from public.profiles
where id = (event->>'user_id')::uuid;
claims := event->'claims';
if user_role is not null then
claims := jsonb_set(claims, '{role}', to_jsonb(user_role));
claims := jsonb_set(claims, '{org_id}', to_jsonb(user_org_id));
else
claims := jsonb_set(claims, '{role}', 'null');
claims := jsonb_set(claims, '{org_id}', 'null');
end if;
event := jsonb_set(event, '{claims}', claims);
return event;
end;
$$;
-- The hook runs as the supabase_auth_admin role, not as the logged-in
-- user, so it needs explicit permission to call the function and read
-- profiles. Everyone else is explicitly blocked from calling it directly.
grant usage on schema public to supabase_auth_admin;
grant execute
on function public.custom_access_token_hook
to supabase_auth_admin;
revoke execute
on function public.custom_access_token_hook
from authenticated, anon, public;
grant select
on table public.profiles
to supabase_auth_admin;
create policy "Allow auth admin to read profiles for JWT hook"
on public.profiles
as permissive
for select
to supabase_auth_admin
using (true);
-- ============================================================
-- Dashboard steps to enable this hook (run the SQL above first):
--
-- 1. Supabase dashboard → Authentication → Hooks (left sidebar,
-- under "Configuration").
-- 2. Find "Customize Access Token (JWT) Claims hook".
-- 3. Choose "Postgres function" as the hook type.
-- 4. Select public.custom_access_token_hook from the dropdown.
-- 5. Enable the hook (toggle on) and save.
-- 6. Existing logged-in sessions keep their OLD token until they
-- refresh/re-login — log out and back in to see new claims.
-- ============================================================