-- ============================================================ -- Time Tracker App - Teams & Privileges Schema -- Run this SQL in your Supabase SQL Editor -- ============================================================ -- 1. Teams table CREATE TABLE IF NOT EXISTS teams ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, name TEXT NOT NULL, description TEXT DEFAULT '', created_at TIMESTAMPTZ DEFAULT now(), created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL ); -- 2. Team Members table (links users to teams with roles) CREATE TABLE IF NOT EXISTS team_members ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('admin', 'manager', 'user')), joined_at TIMESTAMPTZ DEFAULT now(), UNIQUE(team_id, user_id) ); -- 3. User Profiles table (extends auth.users with global role) CREATE TABLE IF NOT EXISTS user_profiles ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, full_name TEXT DEFAULT '', phone TEXT DEFAULT '', company TEXT DEFAULT '', avatar_url TEXT DEFAULT '', global_role TEXT NOT NULL DEFAULT 'user' CHECK (global_role IN ('admin', 'manager', 'user')), created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); -- ============================================================ -- Indexes -- ============================================================ CREATE INDEX IF NOT EXISTS idx_team_members_team_id ON team_members(team_id); CREATE INDEX IF NOT EXISTS idx_team_members_user_id ON team_members(user_id); CREATE INDEX IF NOT EXISTS idx_user_profiles_global_role ON user_profiles(global_role); -- ============================================================ -- Row Level Security (RLS) -- ============================================================ -- Teams: anyone authenticated can read; only admins can insert/update/delete ALTER TABLE teams ENABLE ROW LEVEL SECURITY; CREATE POLICY "Teams are viewable by authenticated users" ON teams FOR SELECT TO authenticated USING (true); CREATE POLICY "Admins can insert teams" ON teams FOR INSERT TO authenticated WITH CHECK ( EXISTS ( SELECT 1 FROM user_profiles WHERE id = auth.uid() AND global_role = 'admin' ) ); CREATE POLICY "Admins can update teams" ON teams FOR UPDATE TO authenticated USING ( EXISTS ( SELECT 1 FROM user_profiles WHERE id = auth.uid() AND global_role = 'admin' ) ); CREATE POLICY "Admins can delete teams" ON teams FOR DELETE TO authenticated USING ( EXISTS ( SELECT 1 FROM user_profiles WHERE id = auth.uid() AND global_role = 'admin' ) ); -- Team Members: authenticated users can read; admins/managers can manage ALTER TABLE team_members ENABLE ROW LEVEL SECURITY; CREATE POLICY "Team members are viewable by authenticated users" ON team_members FOR SELECT TO authenticated USING (true); CREATE POLICY "Admins can insert team members" ON team_members FOR INSERT TO authenticated WITH CHECK ( EXISTS ( SELECT 1 FROM user_profiles WHERE id = auth.uid() AND global_role IN ('admin', 'manager') ) ); CREATE POLICY "Admins can update team members" ON team_members FOR UPDATE TO authenticated USING ( EXISTS ( SELECT 1 FROM user_profiles WHERE id = auth.uid() AND global_role IN ('admin', 'manager') ) ); CREATE POLICY "Admins can delete team members" ON team_members FOR DELETE TO authenticated USING ( EXISTS ( SELECT 1 FROM user_profiles WHERE id = auth.uid() AND global_role IN ('admin', 'manager') ) ); -- User Profiles: users can read all profiles, update their own; admins can update any ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY; CREATE POLICY "User profiles are viewable by authenticated users" ON user_profiles FOR SELECT TO authenticated USING (true); CREATE POLICY "Users can update own profile" ON user_profiles FOR UPDATE TO authenticated USING (auth.uid() = id); CREATE POLICY "Admins can update any profile" ON user_profiles FOR UPDATE TO authenticated USING ( EXISTS ( SELECT 1 FROM user_profiles WHERE id = auth.uid() AND global_role = 'admin' ) ); CREATE POLICY "Users can insert own profile" ON user_profiles FOR INSERT TO authenticated WITH CHECK (auth.uid() = id); -- ============================================================ -- Trigger: Auto-create user_profiles on signup -- ============================================================ CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS TRIGGER AS $$ BEGIN INSERT INTO public.user_profiles (id, full_name, avatar_url) VALUES ( NEW.id, COALESCE(NEW.raw_user_meta_data->>'full_name', ''), COALESCE(NEW.raw_user_meta_data->>'avatar_url', '') ); RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); -- ============================================================ -- Trigger: Auto-update updated_at on user_profiles -- ============================================================ CREATE OR REPLACE FUNCTION public.update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS update_user_profiles_updated_at ON user_profiles; CREATE TRIGGER update_user_profiles_updated_at BEFORE UPDATE ON user_profiles FOR EACH ROW EXECUTE FUNCTION public.update_updated_at(); -- ============================================================ -- Seed: Make the first user an admin (adjust the user_id as needed) -- ============================================================ -- UPDATE user_profiles SET global_role = 'admin' WHERE id = 'YOUR_USER_ID_HERE';