338 lines
15 KiB
PL/PgSQL
338 lines
15 KiB
PL/PgSQL
-- ── Migration 015: Email as universal user identity ───────────────────────────
|
|
--
|
|
-- Background
|
|
-- ----------
|
|
-- Prior migrations stored Clerk opaque user IDs (e.g. `user_2abc…`) in every
|
|
-- `user_id` / `owner_id` / `author_id` column. Those IDs are internal to Clerk
|
|
-- and cannot be resolved back to humans without an extra Clerk API call. The
|
|
-- only durable, human-readable identity available in this stack is the user's
|
|
-- primary email address.
|
|
--
|
|
-- What this migration does
|
|
-- ------------------------
|
|
-- 1. Renames identity columns in all tables:
|
|
-- team_members.user_id → email
|
|
-- workspaces.owner_id → owner_email
|
|
-- intent_diffs.author_id → author_email
|
|
--
|
|
-- 2. Drops all stale RLS policies that referenced the old column names or
|
|
-- used `auth.uid()::text` (which returns a UUID-shaped sub claim, not an
|
|
-- email) and recreates every policy to compare against
|
|
-- `auth.jwt() ->> 'email'` — the email claim injected by the Clerk
|
|
-- Supabase JWT template.
|
|
--
|
|
-- 3. Updates the `is_workspace_member` helper function from migration 003
|
|
-- to use the renamed column.
|
|
--
|
|
-- Application changes (applied in tandem)
|
|
-- ----------------------------------------
|
|
-- • API routes use currentUser().primaryEmailAddress.emailAddress instead of
|
|
-- auth().userId for all DB identity lookups.
|
|
-- • The invite UI accepts an email address instead of a Clerk user ID.
|
|
--
|
|
-- Idempotency
|
|
-- -----------
|
|
-- Column renames are guarded by DO $$ blocks that skip if the target column
|
|
-- already exists. Policy drops use DROP … IF EXISTS. Safe to re-run.
|
|
|
|
-- ══════════════════════════════════════════════════════════════════════════════
|
|
-- 1. Column renames
|
|
-- ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
-- ── team_members: user_id → email ────────────────────────────────────────────
|
|
|
|
DO $$ BEGIN
|
|
IF EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_name = 'team_members' AND column_name = 'user_id'
|
|
) AND NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_name = 'team_members' AND column_name = 'email'
|
|
) THEN
|
|
ALTER TABLE team_members RENAME COLUMN user_id TO email;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Replace the unique constraint (workspace_id, user_id) with (workspace_id, email).
|
|
ALTER TABLE team_members
|
|
DROP CONSTRAINT IF EXISTS team_members_workspace_id_user_id_key;
|
|
|
|
DO $$ BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint
|
|
WHERE conname = 'team_members_workspace_id_email_key'
|
|
) THEN
|
|
ALTER TABLE team_members
|
|
ADD CONSTRAINT team_members_workspace_id_email_key UNIQUE (workspace_id, email);
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Replace the index.
|
|
DROP INDEX IF EXISTS team_members_user_id_idx;
|
|
CREATE INDEX IF NOT EXISTS team_members_email_idx ON team_members (email);
|
|
|
|
-- ── workspaces: owner_id → owner_email ───────────────────────────────────────
|
|
|
|
DO $$ BEGIN
|
|
IF EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_name = 'workspaces' AND column_name = 'owner_id'
|
|
) AND NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_name = 'workspaces' AND column_name = 'owner_email'
|
|
) THEN
|
|
ALTER TABLE workspaces RENAME COLUMN owner_id TO owner_email;
|
|
END IF;
|
|
END $$;
|
|
|
|
DROP INDEX IF EXISTS workspaces_owner_id_idx;
|
|
CREATE INDEX IF NOT EXISTS workspaces_owner_email_idx ON workspaces (owner_email);
|
|
|
|
-- ── intent_diffs: author_id → author_email ───────────────────────────────────
|
|
|
|
DO $$ BEGIN
|
|
IF EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_name = 'intent_diffs' AND column_name = 'author_id'
|
|
) AND NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_name = 'intent_diffs' AND column_name = 'author_email'
|
|
) THEN
|
|
ALTER TABLE intent_diffs RENAME COLUMN author_id TO author_email;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ══════════════════════════════════════════════════════════════════════════════
|
|
-- 2. Drop all stale RLS policies
|
|
-- ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
-- workspaces
|
|
DROP POLICY IF EXISTS workspaces_select ON workspaces;
|
|
DROP POLICY IF EXISTS workspaces_insert ON workspaces;
|
|
DROP POLICY IF EXISTS workspaces_update ON workspaces;
|
|
DROP POLICY IF EXISTS "workspace owner access" ON workspaces;
|
|
|
|
-- artboards
|
|
DROP POLICY IF EXISTS artboards_select ON artboards;
|
|
DROP POLICY IF EXISTS artboards_insert ON artboards;
|
|
DROP POLICY IF EXISTS artboards_update ON artboards;
|
|
DROP POLICY IF EXISTS artboards_delete ON artboards;
|
|
DROP POLICY IF EXISTS "workspace member artboard access" ON artboards;
|
|
|
|
-- origins
|
|
DROP POLICY IF EXISTS "workspace member origin access" ON origins;
|
|
|
|
-- intent_diffs
|
|
DROP POLICY IF EXISTS intent_diffs_select ON intent_diffs;
|
|
DROP POLICY IF EXISTS intent_diffs_insert ON intent_diffs;
|
|
DROP POLICY IF EXISTS intent_diffs_update ON intent_diffs;
|
|
DROP POLICY IF EXISTS "workspace member diff access" ON intent_diffs;
|
|
|
|
-- agent_sessions
|
|
DROP POLICY IF EXISTS agent_sessions_select ON agent_sessions;
|
|
DROP POLICY IF EXISTS agent_sessions_insert ON agent_sessions;
|
|
DROP POLICY IF EXISTS agent_sessions_update ON agent_sessions;
|
|
DROP POLICY IF EXISTS "workspace member session access" ON agent_sessions;
|
|
|
|
-- design_language_files
|
|
DROP POLICY IF EXISTS dlf_select ON design_language_files;
|
|
DROP POLICY IF EXISTS dlf_insert ON design_language_files;
|
|
DROP POLICY IF EXISTS dlf_update ON design_language_files;
|
|
DROP POLICY IF EXISTS "workspace member dlf access" ON design_language_files;
|
|
DROP POLICY IF EXISTS dlf_workspace_select ON design_language_files;
|
|
DROP POLICY IF EXISTS dlf_workspace_write ON design_language_files;
|
|
|
|
-- team_members
|
|
DROP POLICY IF EXISTS team_members_select ON team_members;
|
|
DROP POLICY IF EXISTS team_members_insert ON team_members;
|
|
DROP POLICY IF EXISTS team_members_delete ON team_members;
|
|
|
|
-- design_languages (migration 014)
|
|
DROP POLICY IF EXISTS dl_workspace_member_select ON design_languages;
|
|
DROP POLICY IF EXISTS dl_workspace_member_write ON design_languages;
|
|
DROP POLICY IF EXISTS "design_languages_workspace_member_select" ON design_languages;
|
|
DROP POLICY IF EXISTS "design_languages_workspace_member_insert" ON design_languages;
|
|
DROP POLICY IF EXISTS "design_languages_workspace_member_update" ON design_languages;
|
|
|
|
-- design_language_versions (migration 014)
|
|
DROP POLICY IF EXISTS dlv_workspace_member_select ON design_language_versions;
|
|
DROP POLICY IF EXISTS "design_language_versions_workspace_member_select" ON design_language_versions;
|
|
DROP POLICY IF EXISTS "design_language_versions_workspace_member_insert" ON design_language_versions;
|
|
|
|
-- ══════════════════════════════════════════════════════════════════════════════
|
|
-- 3. Update the is_workspace_member helper (migration 003)
|
|
-- ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
-- Recreate with team_members.email and auth.jwt() ->> 'email'.
|
|
CREATE OR REPLACE FUNCTION is_workspace_member(ws_id UUID)
|
|
RETURNS BOOLEAN LANGUAGE sql SECURITY DEFINER AS $$
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM team_members
|
|
WHERE workspace_id = ws_id
|
|
AND email = auth.jwt() ->> 'email'
|
|
);
|
|
$$;
|
|
|
|
-- ══════════════════════════════════════════════════════════════════════════════
|
|
-- 4. Recreate all RLS policies with email-based identity
|
|
-- ══════════════════════════════════════════════════════════════════════════════
|
|
--
|
|
-- Identity predicate throughout: auth.jwt() ->> 'email'
|
|
-- This reads the `email` claim from the Clerk Supabase JWT template.
|
|
--
|
|
-- For workspace owners the check is: workspaces.owner_email = auth.jwt() ->> 'email'
|
|
-- For general membership: team_members.email = auth.jwt() ->> 'email'
|
|
|
|
-- ── workspaces ────────────────────────────────────────────────────────────────
|
|
|
|
CREATE POLICY workspaces_select ON workspaces
|
|
FOR SELECT
|
|
USING (is_workspace_member(id));
|
|
|
|
CREATE POLICY workspaces_insert ON workspaces
|
|
FOR INSERT
|
|
WITH CHECK (owner_email = auth.jwt() ->> 'email');
|
|
|
|
CREATE POLICY workspaces_update ON workspaces
|
|
FOR UPDATE
|
|
USING (owner_email = auth.jwt() ->> 'email');
|
|
|
|
-- ── artboards ─────────────────────────────────────────────────────────────────
|
|
|
|
CREATE POLICY artboards_select ON artboards
|
|
FOR SELECT USING (is_workspace_member(workspace_id));
|
|
|
|
CREATE POLICY artboards_insert ON artboards
|
|
FOR INSERT WITH CHECK (is_workspace_member(workspace_id));
|
|
|
|
CREATE POLICY artboards_update ON artboards
|
|
FOR UPDATE USING (is_workspace_member(workspace_id));
|
|
|
|
CREATE POLICY artboards_delete ON artboards
|
|
FOR DELETE USING (is_workspace_member(workspace_id));
|
|
|
|
-- ── origins ───────────────────────────────────────────────────────────────────
|
|
-- Accepts both FK directions (spec: origins.artboard_id; legacy: artboards.origin_id).
|
|
|
|
CREATE POLICY "workspace member origin access" ON origins
|
|
FOR ALL
|
|
USING (
|
|
-- spec-compliant: origins.artboard_id set to the artboard's uuid
|
|
artboard_id IN (
|
|
SELECT id FROM artboards
|
|
WHERE is_workspace_member(workspace_id)
|
|
)
|
|
OR
|
|
-- legacy: artboards.origin_id points at this origin row
|
|
id IN (
|
|
SELECT origin_id FROM artboards
|
|
WHERE is_workspace_member(workspace_id)
|
|
AND origin_id IS NOT NULL
|
|
)
|
|
);
|
|
|
|
-- ── intent_diffs ──────────────────────────────────────────────────────────────
|
|
|
|
CREATE POLICY intent_diffs_select ON intent_diffs
|
|
FOR SELECT
|
|
USING (is_workspace_member(
|
|
(SELECT workspace_id FROM artboards WHERE id = artboard_id)
|
|
));
|
|
|
|
CREATE POLICY intent_diffs_insert ON intent_diffs
|
|
FOR INSERT
|
|
WITH CHECK (is_workspace_member(
|
|
(SELECT workspace_id FROM artboards WHERE id = artboard_id)
|
|
));
|
|
|
|
CREATE POLICY intent_diffs_update ON intent_diffs
|
|
FOR UPDATE
|
|
USING (is_workspace_member(
|
|
(SELECT workspace_id FROM artboards WHERE id = artboard_id)
|
|
));
|
|
|
|
-- ── agent_sessions ────────────────────────────────────────────────────────────
|
|
|
|
CREATE POLICY agent_sessions_select ON agent_sessions
|
|
FOR SELECT
|
|
USING (is_workspace_member(
|
|
(SELECT workspace_id FROM artboards WHERE id = artboard_id)
|
|
));
|
|
|
|
CREATE POLICY agent_sessions_insert ON agent_sessions
|
|
FOR INSERT
|
|
WITH CHECK (is_workspace_member(
|
|
(SELECT workspace_id FROM artboards WHERE id = artboard_id)
|
|
));
|
|
|
|
CREATE POLICY agent_sessions_update ON agent_sessions
|
|
FOR UPDATE
|
|
USING (is_workspace_member(
|
|
(SELECT workspace_id FROM artboards WHERE id = artboard_id)
|
|
));
|
|
|
|
-- ── design_language_files ─────────────────────────────────────────────────────
|
|
|
|
CREATE POLICY dlf_workspace_select ON design_language_files
|
|
FOR SELECT
|
|
USING (is_workspace_member(workspace_id));
|
|
|
|
CREATE POLICY dlf_workspace_write ON design_language_files
|
|
FOR ALL
|
|
USING (
|
|
workspace_id IN (
|
|
SELECT workspace_id FROM team_members
|
|
WHERE email = auth.jwt() ->> 'email'
|
|
AND role IN ('OWNER', 'DESIGNER')
|
|
)
|
|
);
|
|
|
|
-- ── team_members ──────────────────────────────────────────────────────────────
|
|
|
|
CREATE POLICY team_members_select ON team_members
|
|
FOR SELECT USING (is_workspace_member(workspace_id));
|
|
|
|
-- Only workspace owners can add members.
|
|
CREATE POLICY team_members_insert ON team_members
|
|
FOR INSERT
|
|
WITH CHECK (
|
|
EXISTS (
|
|
SELECT 1 FROM workspaces
|
|
WHERE id = workspace_id
|
|
AND owner_email = auth.jwt() ->> 'email'
|
|
)
|
|
);
|
|
|
|
-- Owners can remove anyone; members can remove themselves.
|
|
CREATE POLICY team_members_delete ON team_members
|
|
FOR DELETE
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM workspaces
|
|
WHERE id = workspace_id
|
|
AND owner_email = auth.jwt() ->> 'email'
|
|
)
|
|
OR email = auth.jwt() ->> 'email'
|
|
);
|
|
|
|
-- ── design_languages ──────────────────────────────────────────────────────────
|
|
|
|
CREATE POLICY dl_workspace_member_select ON design_languages
|
|
FOR SELECT
|
|
USING (is_workspace_member(workspace_id));
|
|
|
|
CREATE POLICY dl_workspace_member_write ON design_languages
|
|
FOR ALL
|
|
USING (is_workspace_member(workspace_id));
|
|
|
|
-- ── design_language_versions ──────────────────────────────────────────────────
|
|
|
|
CREATE POLICY dlv_workspace_member_select ON design_language_versions
|
|
FOR SELECT
|
|
USING (
|
|
design_language_id IN (
|
|
SELECT dl.id FROM design_languages dl
|
|
WHERE is_workspace_member(dl.workspace_id)
|
|
)
|
|
);
|