-- ══════════════════════════════════════════════════════════════════════ -- HW Collector Hub – Multi-Collection Migration -- Run this in the Supabase SQL Editor (one-time). -- ══════════════════════════════════════════════════════════════════════ -- 1) Collections table CREATE TABLE IF NOT EXISTS collections ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, name TEXT NOT NULL, description TEXT, owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, created_at TIMESTAMPTZ DEFAULT now() ); -- 2) Collection members (join table) CREATE TABLE IF NOT EXISTS collection_members ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, collection_id UUID NOT NULL REFERENCES collections(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'member')), created_at TIMESTAMPTZ DEFAULT now(), UNIQUE(collection_id, user_id) ); -- 3) Add collection_id column to hotwheels ALTER TABLE hotwheels ADD COLUMN IF NOT EXISTS collection_id UUID REFERENCES collections(id) ON DELETE CASCADE; -- ── RLS: collections ────────────────────────────────────────────────── ALTER TABLE collections ENABLE ROW LEVEL SECURITY; -- Users can see collections they are a member of. CREATE POLICY "Members can view collections" ON collections FOR SELECT USING ( owner_id = auth.uid() OR id IN ( SELECT collection_id FROM collection_members WHERE user_id = auth.uid() ) ); -- Any authenticated user can create a collection. CREATE POLICY "Authenticated users can create collections" ON collections FOR INSERT WITH CHECK (auth.uid() = owner_id); -- Only the owner can update. CREATE POLICY "Owner can update collection" ON collections FOR UPDATE USING (owner_id = auth.uid()); -- Only the owner can delete. CREATE POLICY "Owner can delete collection" ON collections FOR DELETE USING (owner_id = auth.uid()); -- ── RLS: collection_members ────────────────────────────────────────── ALTER TABLE collection_members ENABLE ROW LEVEL SECURITY; -- Users can see their own memberships (avoids infinite recursion). CREATE POLICY "Users can view own memberships" ON collection_members FOR SELECT USING (user_id = auth.uid()); -- Owner can add members. CREATE POLICY "Owner can add members" ON collection_members FOR INSERT WITH CHECK ( collection_id IN ( SELECT id FROM collections WHERE owner_id = auth.uid() ) ); -- Owner can remove members. CREATE POLICY "Owner can remove members" ON collection_members FOR DELETE USING ( collection_id IN ( SELECT id FROM collections WHERE owner_id = auth.uid() ) OR user_id = auth.uid() -- members can remove themselves ); -- ── RLS: hotwheels (update existing) ──────────────────────────────── -- Drop old policies first (they were user_id based). DROP POLICY IF EXISTS "Enable read access for all users" ON hotwheels; DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON hotwheels; DROP POLICY IF EXISTS "Enable update for users based on user_id" ON hotwheels; DROP POLICY IF EXISTS "Enable delete for users based on user_id" ON hotwheels; -- Members of a collection can see its cars. CREATE POLICY "Collection members can view cars" ON hotwheels FOR SELECT USING ( collection_id IN ( SELECT collection_id FROM collection_members WHERE user_id = auth.uid() ) ); -- Members can add cars. CREATE POLICY "Collection members can insert cars" ON hotwheels FOR INSERT WITH CHECK ( collection_id IN ( SELECT collection_id FROM collection_members WHERE user_id = auth.uid() ) ); -- Members can update cars in their collections. CREATE POLICY "Collection members can update cars" ON hotwheels FOR UPDATE USING ( collection_id IN ( SELECT collection_id FROM collection_members WHERE user_id = auth.uid() ) ); -- Members can delete cars from their collections. CREATE POLICY "Collection members can delete cars" ON hotwheels FOR DELETE USING ( collection_id IN ( SELECT collection_id FROM collection_members WHERE user_id = auth.uid() ) ); -- ══════════════════════════════════════════════════════════════════════ -- MIGRATION HELPER: Move existing cars into a default collection -- for each user that already has cars. -- ══════════════════════════════════════════════════════════════════════ DO $$ DECLARE _user RECORD; _coll UUID; BEGIN FOR _user IN SELECT DISTINCT user_id FROM hotwheels WHERE collection_id IS NULL LOOP -- Create a default "My Collection" for this user. INSERT INTO collections (name, owner_id) VALUES ('My Collection', _user.user_id) RETURNING id INTO _coll; -- Owner is also a member. INSERT INTO collection_members (collection_id, user_id, role) VALUES (_coll, _user.user_id, 'owner'); -- Assign all existing cars to this collection. UPDATE hotwheels SET collection_id = _coll WHERE user_id = _user.user_id AND collection_id IS NULL; END LOOP; END $$; -- After migration, make collection_id NOT NULL. ALTER TABLE hotwheels ALTER COLUMN collection_id SET NOT NULL; -- ══════════════════════════════════════════════════════════════════════ -- RPC: Look up a user's ID by email (for inviting) -- ══════════════════════════════════════════════════════════════════════ CREATE OR REPLACE FUNCTION get_user_id_by_email(lookup_email TEXT) RETURNS UUID LANGUAGE sql SECURITY DEFINER -- runs as postgres, can read auth.users SET search_path = '' AS $$ SELECT id FROM auth.users WHERE email = lower(lookup_email) LIMIT 1; $$; -- ══════════════════════════════════════════════════════════════════════ -- RPC: Get collection members with their emails -- ══════════════════════════════════════════════════════════════════════ CREATE OR REPLACE FUNCTION get_collection_members(p_collection_id UUID) RETURNS TABLE( id UUID, user_id UUID, email TEXT, role TEXT, created_at TIMESTAMPTZ ) LANGUAGE sql SECURITY DEFINER SET search_path = '' AS $$ SELECT cm.id, cm.user_id, u.email, cm.role, cm.created_at FROM public.collection_members cm JOIN auth.users u ON u.id = cm.user_id WHERE cm.collection_id = p_collection_id AND cm.collection_id IN ( SELECT cm2.collection_id FROM public.collection_members cm2 WHERE cm2.user_id = auth.uid() ) ORDER BY CASE cm.role WHEN 'owner' THEN 0 ELSE 1 END, cm.created_at; $$; -- ══════════════════════════════════════════════════════════════════════ -- Community reporting: car_reports -- ══════════════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS car_reports ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, hw_id VARCHAR NOT NULL REFERENCES global_cars(hw_id) ON DELETE CASCADE, hotwheels_id BIGINT REFERENCES hotwheels(id) ON DELETE SET NULL, reporter_user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, reason TEXT NOT NULL, note TEXT, status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'reviewed', 'resolved', 'dismissed')), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE car_reports ENABLE ROW LEVEL SECURITY; -- Reporters can create reports only for themselves. CREATE POLICY "Users can create own car reports" ON car_reports FOR INSERT WITH CHECK (reporter_user_id = auth.uid()); -- Reporters can see their own reports. CREATE POLICY "Users can view own car reports" ON car_reports FOR SELECT USING (reporter_user_id = auth.uid());