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
80 lines
2.5 KiB
PL/PgSQL
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.
|
|
-- ============================================================
|