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
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
680b6a0194
commit
f539d43136
@@ -0,0 +1,79 @@
|
||||
-- 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.
|
||||
-- ============================================================
|
||||
Reference in New Issue
Block a user